Skip to content

What is Billuu

Source: g2labs-in/billuu-docs@7780d43 · content/start/what-is-billuu.md · updated 2026-09-26

The product in one paragraph

Billuu is a multi-tenant point of sale for cafes and restaurants, built by BrewLabs. One backend deployment serves many businesses at the same time. Each business gets its own PostgreSQL schema inside one database. A business runs one or more outlets. Staff take orders on a POS screen, the order goes to a kitchen display, and the cashier settles the bill and prints it. Customers can also scan a QR code on the table and order from their own phone.

The repositories

The workspace at /home/codejack/code/opencode/billuu holds the applications below.

RepositoryStackEntry pointRole in the product
brewlabs-billu-backendExpress, Sequelize, PostgreSQLapp.jsThe API and the system of record. Owns every table and every rule.
brewlabs-billu-frontendReact 18, Vite 6, Tailwind, Redux Toolkitsrc/App.jsxThe POS and back office screens that staff use.
billuu_landingPageNext.js 14app/page.tsxThe public marketing site.
brewlabs-billu-crmExpress, Sequelizesrc/index.jsCustomer messaging. Serves /api/customers, /api/segments, /api/campaigns.
brewlabs-billu-adminReact 19, Vite 6, Tailwind, Redux Toolkit, MUIsrc/main.jsxThe super admin console, live at superadmin.billuu.com. Cloned locally as breww-admin, and the POS refers to it as pos-admin in a comment in brewlabs-billu-frontend/src/App.jsx.

Two more things sit outside this directory and matter to your mental model. Deployment is driven from a separate infrastructure repository at /home/codejack/code/linode, and the application repositories also carry their own compose files, for example brewlabs-billu-backend/docker-compose.yml and billuu_landingPage/dokploy.yml.

The super admin console is the least finished of the five. It is a separate React app in breww-admin, it deploys on its own, and it calls 142 endpoints while the backend serves 21, so most screens are empty because nothing is behind them rather than because they are broken. It says so on the page. The Super admin panel section of this site covers what it does, how it deploys, and what is missing behind it.

The console moved to g2labs-in/brewlabs-billu-admin on 2026-09-26. Its history before that is in anonymousabhi13/breww-admin, which is also where the older Vercel deployment came from.

Who uses Billuu

Roles are declared once in src/constants/permissions.js in the backend, under ROLES. ROLE_HIERARCHY in the same file assigns each role a numeric level and a canManage list.

ActorRole constantLevelWhat they do
Platform super adminSUPER_ADMIN100Runs the platform across all tenants. Manages tenants, plans and system settings. Signs in with panelType ADMIN.
Business ownerOWNER90Owns one business. Manages outlets, staff, menu, inventory and billing.
Outlet adminADMIN80Same powers as OWNER in practice, because normalizeRole folds ADMIN into OWNER.
Outlet managerMANAGER70Runs a single outlet. Full order, menu, inventory and staff access for that outlet. Cannot create outlets.
CaptainCAPTAIN60Takes orders, applies discounts, settles bills, watches the kitchen display.
CashierCASHIER50Takes orders and settles bills. Read only on menu and customers.
WaiterWAITER45Takes and edits orders and manages tables. Explicitly has no SETTLE_ORDER and no PROCESS_PAYMENT.
Kitchen staffKITCHEN_STAFF40Only three permissions, VIEW_KDS, UPDATE_KDS_STATUS and MARK_ITEM_READY.
Floor staffSTAFFnoneDeclared in ROLES but absent from ROLE_PERMISSIONS. Its menu permissions are the intersection of its own permissions and the admin’s permissions.
Customerno rolenoneHas no User row. Orders through a guest session and a session token.

The customer is the actor that surprises people. A walk in customer at a table is represented by a GuestSession row and a session token, not by a User row. The token is checked by validateCustomerSession in src/middlewares/sessionMiddleware.js and gates every route in src/routes/customerOrderRoutes.js.

The tenant hierarchy

Three identifiers stack on top of each other. Only one of them names the schema.

LevelModelTableNotes
BusinessBusiness in src/control_plane_models/businessModel.jsbusinessesThe commercial account and the subscription owner. type is SOLO or FRANCHISE. parent_business_id links a franchise to its parent.
OrganizationOrganization in src/control_plane_models/organizationModel.jsorganizationsThe tenant boundary. Its id names the schema. type is single, franchise or chain. Carries parent_organization_id, hierarchy_level and hierarchy_path.
OutletOutlet in src/models/outletModel.jsoutlets inside tenant_<organizationId>One physical shop. organization_id is allowNull: false. Also carries is_main, is_head_office and parent_outlet_id.

The tenant key is the organization id. UnifiedTenantMiddleware.resolveOrganizationContext in src/core/tenant/unifiedTenantMiddleware.js computes organizationId = user.organizationId || user.businessId and returns schemaName as `${TENANT_SCHEMA_PREFIX}${organizationId}`. TENANT_SCHEMA_PREFIX is tenant_ in src/utils/constants.js. So a tenant schema is named tenant_<organizationId>.

public.tenant_registries records each tenant. TenantRegistry in src/control_plane_models/tenantRegistryModel.js stores tenant_id described as “Link to organization/franchise”, plus the unique schema_name and a status that is one of ACTIVE, READY, PENDING, CREATING, ONBOARDING, SUSPENDED, INACTIVE, INIT_FAILED, INIT_IN_PROGRESS, TRIAL.

Franchise

A franchise is an organization tree, not a separate kind of account.

FieldMeaning
Organization.parentOrganizationIdNull for the root franchise, set for a sub franchise.
Organization.isRoot and Organization.isSubFranchiseVirtual fields. They read parentOrganizationId and are not stored.
Organization.hierarchyLevel0 for root, 1 for a child, 2 for a grandchild.
Organization.hierarchyPathDot separated path used for tree traversal.
Organization.permissionModeINHERIT, ISOLATED or OVERRIDE. Default INHERIT.

Permissions for franchise owners are VIEW_SUB_FRANCHISES, MANAGE_SUB_FRANCHISES, CREATE_SUB_FRANCHISE, EDIT_SUB_FRANCHISE and DELETE_SUB_FRANCHISE. OWNER and MANAGER both hold the first two. Super admin holds the franchise linking permissions LINK_FRANCHISE, MOVE_FRANCHISE, UNLINK_FRANCHISE, VIEW_FRANCHISE_HIERARCHY and MANAGE_FRANCHISE_HIERARCHY.

Two planes, two middleware chains

The backend splits every route into a control plane chain and a tenant chain. The split is declared in src/architecture/databaseMiddlewareChain.js.

ChainBuilt fromMounted at
adminChainauthenticate, then adminOnly/api/admin
tenantChainauthenticate, then setupTenantContext, then modelsSafetyLayer/api/tenant, /api/inventory, /api/reports, franchise routes
publicChaincookie parser and a response wrapper, no auth/api/auth, /api/super-admin, /api/user, /api/upload, /api/menu, /api/qr, /api/sessions, /api/customer, /api/onboarding

Control plane tables live in the public schema. Their models are listed in CONTROL_MODELS in src/utils/constants.js and include Business, User, Organization, TenantRegistry, Plan, Subscription, SuperAdminUser, OutletUser, PermissionOverride and UserMenuPermission. Tenant tables live in the tenant schema and are listed in TENANT_MODELS in the same file. Both arrays are frozen with Object.freeze.

Authorization travels in the JWT, not in the URL. panelType is either ADMIN or TENANT. src/core/auth/authService.js sets panelType: isSuperAdmin ? 'ADMIN' : 'TENANT', and src/core/middleware/tokenVerification.js repeats that fallback and rejects the wrong panel with a 403. The POS frontend refuses to run for a super admin at all, and refuses any token whose panelType is not TENANT, in src/App.jsx.

Core loop 1, taking an order

  1. A staff member opens /tables. The route is wrapped in ProtectedRoute menuKey="tables" in src/App.jsx.
  2. They pick a free table. src/pages/Tables.jsx loads the existing order into the Redux cart when the table is occupied, or builds a new one when it is not.
  3. The app navigates to /menu, guarded by ProtectedRoute menuKey="menu", where items are added to the cart.
  4. The order is saved. src/models/orderModel.js defaults status to PENDING and type to DINE_IN. The table itself disagrees, because src/migrations/tenant/005_order_tables.js creates orders.status with DEFAULT 'KOT_SENT'. Order types are DINE_IN, TAKEAWAY and DELIVERY, and each has its own creation permission, CREATE_DINE_IN_ORDER, CREATE_TAKEAWAY_ORDER and CREATE_DELIVERY_ORDER.

Core loop 2, sending the order to the kitchen

The order screen src/pages/OrderDetails.jsx offers four actions, Save Change, Print Invoice, Send KOT and Settle Bill.

Send KOT sets the order status to KOT_SENT. The kitchen display then picks the order up. getKitchenOrders in src/modules/orders/controllers/kitchenController.js selects orders whose status is in KOT_SENT, PREPARING or READY, oldest first, and returns the items with their products. Kitchen status changes go through updateKitchenOrderStatus.

The legal transitions live in STATUS_FLOW in src/config/orderStatuses.js.

FromAllowed next states
PENDINGCREATED, KOT_SENT, CANCELLED, COMPLETED, CLOSED
CREATEDKOT_SENT, CANCELLED, COMPLETED, CLOSED
KOT_SENTIN_PROGRESS, CANCELLED, COMPLETED, CLOSED
IN_PROGRESSCOMPLETED, CLOSED, CANCELLED
PREPARINGREADY, SERVED, COMPLETED, CLOSED, CANCELLED
READYSERVED, COMPLETED, CLOSED, CANCELLED
SERVEDCOMPLETED, CLOSED, CANCELLED
COMPLETEDCLOSED, ARCHIVED
CLOSEDARCHIVED
CANCELLEDterminal
ARCHIVEDterminal

Core loop 3, billing and payment

Settle Bill calls settleOrder from src/pages/OrderDetails.jsx, sets the local status to CLOSED, and sends a payment method with the request. Print Invoice renders the bill from the tenant’s BillingConfig row, which holds the shop name, address, GST number, tax_rate, service_charge_rate, paper_size, theme_color, footer_text and upi_id.

Two payment paths exist.

PathModel and fieldsWhere it is handled
GatewayPayment with gateway defaulting to RAZORPAY, plus payment_id, order_id, amount, currency, status and methodsrc/models/paymentModel.js
Manual UPI proofPayment with screenshot, utr_number, verification_status, verified_by, verified_at and rejection_reasonPOST /api/customer/orders/:orderId/payment-proof in src/routes/customerOrderRoutes.js, reviewed in src/pages/admin/PaymentVerificationQueue.jsx

The manual path drives its own status chain on the order, CART_CREATED to PAYMENT_PENDING to PAYMENT_VERIFICATION_PENDING to ORDER_CONFIRMED, with REJECTED sending the order back to PAYMENT_PENDING. The fields were added by src/migrations/tenant/030_manual_payment_verification.js.

Core loop 4, the customer orders from a QR code

  1. The shop generates a menu QR from POST /api/qr/menu/generate, called in src/components/shared/Header.jsx.
  2. Scanning it opens a session through src/routes/sessionRoutes.js, which creates or reuses a TableSession and a GuestSession.
  3. The customer reads the menu with GET /api/customer/menu and places the order with POST /api/customer/orders.
  4. The order moves through the manual payment chain above and then rejoins the kitchen loop at KOT_SENT.

TableSession in src/models/tableSessionModel.js is one sitting at one table. It carries session_code, customer_count, subtotal, tax, discount, total, payment_status and order_status. GuestSession in src/models/guestSessionModel.js is one device inside that sitting. It carries device_id, session_token, cart_data, expires_at and active, and it points at the table session.

The supporting loops

AreaWhat it does
InventoryRaw materials in InventoryItem, per product stock in Inventory, recipes in Recipe and RecipeItem, purchase orders and goods receipts, stock transfers, wastage, and FEFO batch expiry tracking added by src/migrations/tenant/033_fefo_batch_columns.js.
MenuCategories, products, product types, variants, modifiers and per outlet availability.
Staff and permissionsPer user menu permissions in user_menu_permissions plus PermissionOverride rows that grant or deny one permission at GLOBAL, ORGANIZATION, OUTLET or USER scope.
AccountingAccount, AccountTransaction, Income, Expense and ExpenseType, all reachable from /admin/accounting.
Customer duesCustomer.total_due and CustomerLedger entries, surfaced at /admin/customer-due.

Traps that catch new engineers

  • menu means two different things. ALL_MENUS in src/config/menuActionsConfig.js is the sidebar navigation. The food menu is the Product, Category and ProductType tables. Both are called menu in the code, and outlets additionally have a menu_version column.
  • Roles are declared more than once with different contents. src/constants/permissions.js lists nine. src/core/rbac/rbacMiddleware.js declares its own list of eleven, adding SUB_OUTLET and PARTNER that the constants file does not define. The same eleven appear again in src/core/middleware/unifiedRbacMiddleware.js, src/middlewares/unifiedRbacMiddleware.js and src/core/auth/unifiedAuthMiddleware.js.
  • ADMIN and OWNER are one role for permissions and two rows in ROLE_HIERARCHY. normalizeRole maps ADMIN to OWNER, so getPermissionsForRole('ADMIN') and getRoleLevel('ADMIN') both read the OWNER entry. The ADMIN row at level 80 is not reached through those helpers.
  • src/middlewares/unifiedRbacMiddleware.js and src/core/middleware/unifiedRbacMiddleware.js are near duplicates of src/core/rbac/rbacMiddleware.js. The tenant routes import the core/rbac copy.
  • The frontend keeps a hand written copy of the backend permission constants in src/constants/permissions.js. It is not generated. LEGACY_ROLE_MAP there maps Staff to ROLES.CASHIER, which the backend does not do.
  • Several controllers build the schema name from businessId rather than the organization id, for example src/controllers/qrController.js and src/controllers/sessionController.js. Those paths are correct only when the user has no separate organization.

Sources

  • brewlabs-billu-backend/app.js
  • brewlabs-billu-backend/src/architecture/databaseMiddlewareChain.js
  • brewlabs-billu-backend/src/config/orderStatuses.js
  • brewlabs-billu-backend/src/constants/permissions.js
  • brewlabs-billu-backend/src/control_plane_models/businessModel.js
  • brewlabs-billu-backend/src/control_plane_models/organizationModel.js
  • brewlabs-billu-backend/src/control_plane_models/outletUserModel.js
  • brewlabs-billu-backend/src/control_plane_models/permissionOverrideModel.js
  • brewlabs-billu-backend/src/control_plane_models/planModel.js
  • brewlabs-billu-backend/src/control_plane_models/tenantRegistryModel.js
  • brewlabs-billu-backend/src/control_plane_models/userModel.js
  • brewlabs-billu-backend/src/core/auth/authService.js
  • brewlabs-billu-backend/src/core/middleware/tokenVerification.js
  • brewlabs-billu-backend/src/core/rbac/rbacMiddleware.js
  • brewlabs-billu-backend/src/core/tenant/unifiedTenantMiddleware.js
  • brewlabs-billu-backend/src/controllers/customerOrderController.js
  • brewlabs-billu-backend/src/controllers/qrController.js
  • brewlabs-billu-backend/src/controllers/sessionController.js
  • brewlabs-billu-backend/src/core/tenant/tenantMiddleware.js
  • brewlabs-billu-backend/src/migrations/tenant/005_order_tables.js
  • brewlabs-billu-backend/src/migrations/tenant/030_manual_payment_verification.js
  • brewlabs-billu-backend/src/migrations/tenant/033_fefo_batch_columns.js
  • brewlabs-billu-backend/src/models/areaModel.js
  • brewlabs-billu-backend/src/models/billingConfigModel.js
  • brewlabs-billu-backend/src/models/customerModel.js
  • brewlabs-billu-backend/src/models/guestSessionModel.js
  • brewlabs-billu-backend/src/models/inventoryBatchModel.js
  • brewlabs-billu-backend/src/models/inventoryItemModel.js
  • brewlabs-billu-backend/src/models/inventoryModel.js
  • brewlabs-billu-backend/src/models/inventoryTransactionModel.js
  • brewlabs-billu-backend/src/models/modifierOptionModel.js
  • brewlabs-billu-backend/src/models/orderModel.js
  • brewlabs-billu-backend/src/models/outletModel.js
  • brewlabs-billu-backend/src/models/paymentModel.js
  • brewlabs-billu-backend/src/models/productModel.js
  • brewlabs-billu-backend/src/models/recipeItemModel.js
  • brewlabs-billu-backend/src/models/recipeModel.js
  • brewlabs-billu-backend/src/models/stockTransactionModel.js
  • brewlabs-billu-backend/src/models/tableModel.js
  • brewlabs-billu-backend/src/models/tableSessionModel.js
  • brewlabs-billu-backend/src/modules/orders/controllers/kitchenController.js
  • brewlabs-billu-backend/src/modules/permissions/README.md
  • brewlabs-billu-backend/src/modules/permissions/menuPermissionService.js
  • brewlabs-billu-backend/src/routes/customerOrderRoutes.js
  • brewlabs-billu-backend/src/routes/saasRoutes.js
  • brewlabs-billu-backend/src/routes/tenantRoutes.js
  • brewlabs-billu-backend/src/tests/security/roleSegregationTests.js
  • brewlabs-billu-backend/src/utils/constants.js
  • brewlabs-billu-backend/src/utils/entityScopeRegistry.js
  • brewlabs-billu-crm/src/index.js
  • billuu_landingPage/app/page.tsx
  • brewlabs-billu-frontend/src/App.jsx
  • brewlabs-billu-frontend/src/components/shared/ProtectedRoute.jsx
  • brewlabs-billu-frontend/src/config/menuActionsConfig.js
  • brewlabs-billu-frontend/src/constants/permissions.js
  • brewlabs-billu-frontend/src/context/PermissionContext.jsx
  • brewlabs-billu-frontend/src/https/axiosWrapper.js
  • brewlabs-billu-frontend/src/pages/Home.jsx
  • brewlabs-billu-frontend/src/pages/OrderDetails.jsx
  • brewlabs-billu-frontend/src/pages/Tables.jsx