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.
| Repository | Stack | Entry point | Role in the product |
|---|---|---|---|
brewlabs-billu-backend | Express, Sequelize, PostgreSQL | app.js | The API and the system of record. Owns every table and every rule. |
brewlabs-billu-frontend | React 18, Vite 6, Tailwind, Redux Toolkit | src/App.jsx | The POS and back office screens that staff use. |
billuu_landingPage | Next.js 14 | app/page.tsx | The public marketing site. |
brewlabs-billu-crm | Express, Sequelize | src/index.js | Customer messaging. Serves /api/customers, /api/segments, /api/campaigns. |
brewlabs-billu-admin | React 19, Vite 6, Tailwind, Redux Toolkit, MUI | src/main.jsx | The 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.
| Actor | Role constant | Level | What they do |
|---|---|---|---|
| Platform super admin | SUPER_ADMIN | 100 | Runs the platform across all tenants. Manages tenants, plans and system settings. Signs in with panelType ADMIN. |
| Business owner | OWNER | 90 | Owns one business. Manages outlets, staff, menu, inventory and billing. |
| Outlet admin | ADMIN | 80 | Same powers as OWNER in practice, because normalizeRole folds ADMIN into OWNER. |
| Outlet manager | MANAGER | 70 | Runs a single outlet. Full order, menu, inventory and staff access for that outlet. Cannot create outlets. |
| Captain | CAPTAIN | 60 | Takes orders, applies discounts, settles bills, watches the kitchen display. |
| Cashier | CASHIER | 50 | Takes orders and settles bills. Read only on menu and customers. |
| Waiter | WAITER | 45 | Takes and edits orders and manages tables. Explicitly has no SETTLE_ORDER and no PROCESS_PAYMENT. |
| Kitchen staff | KITCHEN_STAFF | 40 | Only three permissions, VIEW_KDS, UPDATE_KDS_STATUS and MARK_ITEM_READY. |
| Floor staff | STAFF | none | Declared in ROLES but absent from ROLE_PERMISSIONS. Its menu permissions are the intersection of its own permissions and the admin’s permissions. |
| Customer | no role | none | Has 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.
| Level | Model | Table | Notes |
|---|---|---|---|
| Business | Business in src/control_plane_models/businessModel.js | businesses | The commercial account and the subscription owner. type is SOLO or FRANCHISE. parent_business_id links a franchise to its parent. |
| Organization | Organization in src/control_plane_models/organizationModel.js | organizations | The tenant boundary. Its id names the schema. type is single, franchise or chain. Carries parent_organization_id, hierarchy_level and hierarchy_path. |
| Outlet | Outlet in src/models/outletModel.js | outlets 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.
| Field | Meaning |
|---|---|
Organization.parentOrganizationId | Null for the root franchise, set for a sub franchise. |
Organization.isRoot and Organization.isSubFranchise | Virtual fields. They read parentOrganizationId and are not stored. |
Organization.hierarchyLevel | 0 for root, 1 for a child, 2 for a grandchild. |
Organization.hierarchyPath | Dot separated path used for tree traversal. |
Organization.permissionMode | INHERIT, 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.
| Chain | Built from | Mounted at |
|---|---|---|
adminChain | authenticate, then adminOnly | /api/admin |
tenantChain | authenticate, then setupTenantContext, then modelsSafetyLayer | /api/tenant, /api/inventory, /api/reports, franchise routes |
publicChain | cookie 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
- A staff member opens
/tables. The route is wrapped inProtectedRoute menuKey="tables"insrc/App.jsx. - They pick a free table.
src/pages/Tables.jsxloads the existing order into the Redux cart when the table is occupied, or builds a new one when it is not. - The app navigates to
/menu, guarded byProtectedRoute menuKey="menu", where items are added to the cart. - The order is saved.
src/models/orderModel.jsdefaultsstatustoPENDINGandtypetoDINE_IN. The table itself disagrees, becausesrc/migrations/tenant/005_order_tables.jscreatesorders.statuswithDEFAULT 'KOT_SENT'. Order types areDINE_IN,TAKEAWAYandDELIVERY, and each has its own creation permission,CREATE_DINE_IN_ORDER,CREATE_TAKEAWAY_ORDERandCREATE_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.
| From | Allowed next states |
|---|---|
PENDING | CREATED, KOT_SENT, CANCELLED, COMPLETED, CLOSED |
CREATED | KOT_SENT, CANCELLED, COMPLETED, CLOSED |
KOT_SENT | IN_PROGRESS, CANCELLED, COMPLETED, CLOSED |
IN_PROGRESS | COMPLETED, CLOSED, CANCELLED |
PREPARING | READY, SERVED, COMPLETED, CLOSED, CANCELLED |
READY | SERVED, COMPLETED, CLOSED, CANCELLED |
SERVED | COMPLETED, CLOSED, CANCELLED |
COMPLETED | CLOSED, ARCHIVED |
CLOSED | ARCHIVED |
CANCELLED | terminal |
ARCHIVED | terminal |
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.
| Path | Model and fields | Where it is handled |
|---|---|---|
| Gateway | Payment with gateway defaulting to RAZORPAY, plus payment_id, order_id, amount, currency, status and method | src/models/paymentModel.js |
| Manual UPI proof | Payment with screenshot, utr_number, verification_status, verified_by, verified_at and rejection_reason | POST /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
- The shop generates a menu QR from
POST /api/qr/menu/generate, called insrc/components/shared/Header.jsx. - Scanning it opens a session through
src/routes/sessionRoutes.js, which creates or reuses aTableSessionand aGuestSession. - The customer reads the menu with
GET /api/customer/menuand places the order withPOST /api/customer/orders. - 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
| Area | What it does |
|---|---|
| Inventory | Raw 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. |
| Menu | Categories, products, product types, variants, modifiers and per outlet availability. |
| Staff and permissions | Per user menu permissions in user_menu_permissions plus PermissionOverride rows that grant or deny one permission at GLOBAL, ORGANIZATION, OUTLET or USER scope. |
| Accounting | Account, AccountTransaction, Income, Expense and ExpenseType, all reachable from /admin/accounting. |
| Customer dues | Customer.total_due and CustomerLedger entries, surfaced at /admin/customer-due. |
Traps that catch new engineers
menumeans two different things.ALL_MENUSinsrc/config/menuActionsConfig.jsis the sidebar navigation. The food menu is theProduct,CategoryandProductTypetables. Both are called menu in the code, and outlets additionally have amenu_versioncolumn.- Roles are declared more than once with different contents.
src/constants/permissions.jslists nine.src/core/rbac/rbacMiddleware.jsdeclares its own list of eleven, addingSUB_OUTLETandPARTNERthat the constants file does not define. The same eleven appear again insrc/core/middleware/unifiedRbacMiddleware.js,src/middlewares/unifiedRbacMiddleware.jsandsrc/core/auth/unifiedAuthMiddleware.js. ADMINandOWNERare one role for permissions and two rows inROLE_HIERARCHY.normalizeRolemapsADMINtoOWNER, sogetPermissionsForRole('ADMIN')andgetRoleLevel('ADMIN')both read theOWNERentry. TheADMINrow at level 80 is not reached through those helpers.src/middlewares/unifiedRbacMiddleware.jsandsrc/core/middleware/unifiedRbacMiddleware.jsare near duplicates ofsrc/core/rbac/rbacMiddleware.js. The tenant routes import thecore/rbaccopy.- The frontend keeps a hand written copy of the backend permission constants in
src/constants/permissions.js. It is not generated.LEGACY_ROLE_MAPthere mapsStafftoROLES.CASHIER, which the backend does not do. - Several controllers build the schema name from
businessIdrather than the organization id, for examplesrc/controllers/qrController.jsandsrc/controllers/sessionController.js. Those paths are correct only when the user has no separate organization.
Sources
brewlabs-billu-backend/app.jsbrewlabs-billu-backend/src/architecture/databaseMiddlewareChain.jsbrewlabs-billu-backend/src/config/orderStatuses.jsbrewlabs-billu-backend/src/constants/permissions.jsbrewlabs-billu-backend/src/control_plane_models/businessModel.jsbrewlabs-billu-backend/src/control_plane_models/organizationModel.jsbrewlabs-billu-backend/src/control_plane_models/outletUserModel.jsbrewlabs-billu-backend/src/control_plane_models/permissionOverrideModel.jsbrewlabs-billu-backend/src/control_plane_models/planModel.jsbrewlabs-billu-backend/src/control_plane_models/tenantRegistryModel.jsbrewlabs-billu-backend/src/control_plane_models/userModel.jsbrewlabs-billu-backend/src/core/auth/authService.jsbrewlabs-billu-backend/src/core/middleware/tokenVerification.jsbrewlabs-billu-backend/src/core/rbac/rbacMiddleware.jsbrewlabs-billu-backend/src/core/tenant/unifiedTenantMiddleware.jsbrewlabs-billu-backend/src/controllers/customerOrderController.jsbrewlabs-billu-backend/src/controllers/qrController.jsbrewlabs-billu-backend/src/controllers/sessionController.jsbrewlabs-billu-backend/src/core/tenant/tenantMiddleware.jsbrewlabs-billu-backend/src/migrations/tenant/005_order_tables.jsbrewlabs-billu-backend/src/migrations/tenant/030_manual_payment_verification.jsbrewlabs-billu-backend/src/migrations/tenant/033_fefo_batch_columns.jsbrewlabs-billu-backend/src/models/areaModel.jsbrewlabs-billu-backend/src/models/billingConfigModel.jsbrewlabs-billu-backend/src/models/customerModel.jsbrewlabs-billu-backend/src/models/guestSessionModel.jsbrewlabs-billu-backend/src/models/inventoryBatchModel.jsbrewlabs-billu-backend/src/models/inventoryItemModel.jsbrewlabs-billu-backend/src/models/inventoryModel.jsbrewlabs-billu-backend/src/models/inventoryTransactionModel.jsbrewlabs-billu-backend/src/models/modifierOptionModel.jsbrewlabs-billu-backend/src/models/orderModel.jsbrewlabs-billu-backend/src/models/outletModel.jsbrewlabs-billu-backend/src/models/paymentModel.jsbrewlabs-billu-backend/src/models/productModel.jsbrewlabs-billu-backend/src/models/recipeItemModel.jsbrewlabs-billu-backend/src/models/recipeModel.jsbrewlabs-billu-backend/src/models/stockTransactionModel.jsbrewlabs-billu-backend/src/models/tableModel.jsbrewlabs-billu-backend/src/models/tableSessionModel.jsbrewlabs-billu-backend/src/modules/orders/controllers/kitchenController.jsbrewlabs-billu-backend/src/modules/permissions/README.mdbrewlabs-billu-backend/src/modules/permissions/menuPermissionService.jsbrewlabs-billu-backend/src/routes/customerOrderRoutes.jsbrewlabs-billu-backend/src/routes/saasRoutes.jsbrewlabs-billu-backend/src/routes/tenantRoutes.jsbrewlabs-billu-backend/src/tests/security/roleSegregationTests.jsbrewlabs-billu-backend/src/utils/constants.jsbrewlabs-billu-backend/src/utils/entityScopeRegistry.jsbrewlabs-billu-crm/src/index.jsbilluu_landingPage/app/page.tsxbrewlabs-billu-frontend/src/App.jsxbrewlabs-billu-frontend/src/components/shared/ProtectedRoute.jsxbrewlabs-billu-frontend/src/config/menuActionsConfig.jsbrewlabs-billu-frontend/src/constants/permissions.jsbrewlabs-billu-frontend/src/context/PermissionContext.jsxbrewlabs-billu-frontend/src/https/axiosWrapper.jsbrewlabs-billu-frontend/src/pages/Home.jsxbrewlabs-billu-frontend/src/pages/OrderDetails.jsxbrewlabs-billu-frontend/src/pages/Tables.jsx