Operations and billing for a multi-outlet cafe chain
A back office and a table-service POS for a multi-outlet cafe chain in India. Two Next.js apps on one Postgres database, built alone, with every rule that touches money enforced in the database.
Live at six outlets with no downtime since launch. The chain plans to grow to 10 to 15 outlets on it within a year.
Take an order, send it to the kitchen, print the bill, then close the day. Fictional demo brand, canned menu, under a minute.
Opening the till…
The problem
Each outlet had a manager, a few staff and a counter POS on a paid subscription. Head office had nowhere to see what was happening.
No nightly close at all. Sales, purchases and expenses came in through back-and-forth messages, and nobody counted the cash drawer against an expected figure.
Nobody could say whether an outlet made money. Some owners said theirs ran at a loss; nothing could confirm or disprove it.
Nothing was locked. A figure could change after the fact with no record of who changed it.
Salary was argued over: absences, leave, overtime and advances with no shared working.
The POS was an island. Its sales were re-typed into reports by hand, and its fee grew with every outlet.
What I built
One product, two apps, sharing sign-in, outlets and payment channels. Order takers only ever see billing, and the database itself stops them reading anything else.
Billing and POS, at the counter
Tables and orders
The outlet's tables at a glance: free, busy with a running total, or bill due. Unsent items survive a refresh on the device.
Kitchen tokens
Send to kitchen prints only the new items, numbered per outlet from 1 each day. Prices come from the database at send time, never from the phone.
GST bills
CGST and SGST per outlet, manager-only discounts capped at 15% with a reason, round-up to the rupee, one bill-number series with no gaps.
Silent printing
One computer per outlet prints every slip to an 80mm or 58mm thermal printer, pushed in real time. Everyone can see whether the printer is working.
Operations, for managers and head office
Nightly close
Sales per channel, purchases, expenses and a cash count. The drawer reconciles live, any difference needs a note, and sending locks the day.
Corrections
A sent day reopens only with head office approval, for 24 hours, with every change logged as old value, new value, who and when.
Salary with the working shown
Attendance feeds salary with every figure explained. A locked month stores its numbers so later rule changes never rewrite history.
Head office view
Who hasn't reported, cash differences past a threshold, live sales across outlets, profit and loss per outlet, and an accounting export.
More of the product
Real screens with a fictional demo brand and made-up data.
Architecture
Tap any part to see the decision behind it and what it cost.
People
Two apps, on Vercel
One Supabase project
Postgres
Decision
Writes on money tables go through SQL functions, not table writes. Billing tables have no insert or update policies at all.
Why
Role, outlet, order state and price are checked in one transaction, whichever app calls.
Trade-off accepted
Two codebases depend on one schema, so every migration lives in one place and the generated types are shared.
At the counter
Postgres
Decision
Writes on money tables go through SQL functions, not table writes. Billing tables have no insert or update policies at all.
Why
Role, outlet, order state and price are checked in one transaction, whichever app calls.
Trade-off accepted
Two codebases depend on one schema, so every migration lives in one place and the generated types are shared.
Key decisions and trade-offs
One database for two apps
Why: Bills must flow into the nightly report, and one account works in both apps
Cost: Both codebases depend on one schema
Business rules in Postgres
Why: Access and money rules hold even if someone calls the API directly
Cost: Harder to unit test, covered by a live security suite
Store computed salary, not just inputs
Why: A locked month must read the same after rules change, and they did, twice
Cost: Unlocking a month is head office only and recalculates from scratch
No ORM, validation, state or component library
Why: Small surface, fast pages, one person maintains both apps
Cost: More hand-written code
Push, not poll, for printing
Why: Printer traffic scales with slips, not with time
Cost: A realtime connection to keep healthy
Hardest problems
01Keeping order takers out of the back office
Problem
Billing added a new role that shares outlet membership with managers. Almost every existing policy trusted "is this user linked to this outlet", so an order taker would have inherited access to cash, salary and expenses.
Approach
Audited every policy and storage bucket before the billing schema went in. Rewrote the shared membership helper so it ignores order takers, which closed every policy built on it at once, and gave billing its own helper.
Result
A live test signs in as an order taker and gets zero rows from every back-office table and bucket, and every write is refused. It also closed an older gap: a deactivated manager with a live session could read through the API.
02Bill numbers with no gaps, under concurrent taps
Problem
One bill-number series across all outlets, restarting each April, never reused, never skipping. Several phones and a counter PC sending at once on unreliable connections.
Approach
A counter row incremented and returned in one statement, so its lock queues callers, and called inside the same transaction that writes the bill, so a failure rolls the number back too. Idempotency keys handle the retries.
Result
20 concurrent sends give 20 distinct, consecutive token numbers. Two simultaneous bills with one key return one bill.
03Silent printing on ordinary Windows PCs
Problem
No native agent, non-technical staff, any printer model. On the first real counter PC a dialog flashed, nothing printed, and the Windows queue stayed empty.
Approach
Found that Chrome's kiosk printing uses the last printer chosen in that profile, defaulting to Save as PDF. The app now generates a launcher that writes the Windows default printer into Chrome's settings before starting, refuses PDF and virtual printers, and keeps a minimised window printing.
Result
The one rule for staff became "the bill printer is the Windows default". Slips match the outlets' old printed layouts at 80mm and 58mm.
0496% fewer requests by pushing instead of polling
Problem
Printer computers polled every few seconds. Five printers made about 179K serverless requests a day for about 200 slips.
Approach
Print jobs are pushed in real time. The printer checks in every 10 seconds only while someone else at the outlet is online, every 5 minutes when alone. Links prefetch only on hover or touch.
Result
About 179K requests a day down to 5K to 7K, while the outlets print about 150 slips a day.
05Money that changes after the day is closed
Problem
Bills get cancelled, restored or paid later after the nightly report that counted them was sent and locked. Managers were also typing pay-later bills in by hand, so money was counted twice.
Approach
A change to one bill moves a sent report by exactly that bill's amounts. Hand-typed dues are linked to the matching bills when the customer and the total agree. Each migration was dry-run in a rolled-back transaction first.
Result
One ledger of who owes what across both apps. The linking matched 12 hand-typed dues to 26 bills in existing data.
Outcomes
In use
Six outlets billing live: the first two on 29 Oct 2026, all six by 2 Nov. No downtime since.
Outlets close their day in the app: a locked report, a reconciled drawer, and every difference explained in writing.
The chain can now see that outlets reported as loss-making are not (client).
Built to grow: the chain plans 10 to 15 outlets on it within a year, and a new outlet needs no licence and no deployment.
Under the hood
42 migrations, about 50 tables, about 90 SQL functions, 144 row-level security policies and 18 triggers.
490+ unit tests and a live security suite. Every migration is checked object by object against production.
Serverless traffic cut by 96%, so printing scales with slips, not with time.
Not used yet, and I'd rather say so
Built and tested but not used yet: outlet audits, correction requests, the accounting export (no ledgers set up yet) and order-taker accounts. Managers run the counter themselves for now.
Stack
App
Next.js 16, React 19, TypeScript, Tailwind 4, server actions
Three AI hackathon judges who read your repo before the real ones do. Every README claim is traced to the file that would implement it, you answer three questions against a 30-second clock, and your odds move live. The model never sets a number.
Answers from a team's Slack, Google Drive and Notion. Every source is cited, nobody sees a document they couldn't already open, and each answer says whether it's still true.
0permission leaks found
90%of questions find the right source in the top 8, up from 80%