Canteen
Ordering and dispensing for company canteens. Employees pick lunch on the web or at a kiosk, the kitchen knows the count in advance, and the counter hands a meal out in one card scan.
- Client
- Fellow Bytes, own product
- My role
- Architecture, all four front ends, API
- Timeline
- April 2026 to now
- Status
- In development, offline mode in progress
- Stack
- Next.js 16, React 19, Vite PWAs, NestJS, Prisma, PostgreSQL, Redis, Socket.IO, Python


Why it exists
A company canteen feeds employees of several firms. Each employee is entitled to one subsidised lunch a day, and each firm is billed for how many of those lunches its people ate, not for the amount. That count usually lives on paper lists and in a spreadsheet at the end of the month.
The kitchen doesn’t know how much to cook, the counter looks names up on a printout, and billing is a day of manual counting. Canteen replaces all three with one system that the operator, the kitchen, the counter and the employees each see from their own side.
Four apps, one API
Every role gets the screen it needs and nothing else. All four talk to the same NestJS API. Types, money formatting and domain rules live in shared workspace packages, so the apps can’t drift apart on what a subsidy or a cut-off means.

Counter terminal
- A card scan opens the diner’s orders for today. The reader types like a keyboard, so any USB scanner works.
- Terminals share a live connection. Hand a meal out on one and it disappears from the others, with a target of under 200 ms.
- The subsidised lunch and anything extra are separate orders, because the firm is billed per subsidised meal.
- Handing out is one tap, and the request is idempotent: a double tap or a retry after a dropped connection hands out once.


Employee web
The week ahead, the day’s menu with allergens, a running total that shows what the subsidy covers, and order history with credit.

Kiosk
Scan a card, pick a day and dishes with big touch targets, confirm. Kiosks are paired to the canteen by QR code and rate-limited per device.

Admin
Menus, companies, employees, credit, kiosks, and the monthly billing report that used to be a day of counting. The report is built in the background and arrives as an XLSX.
How it fits together
Decisions worth mentioning
- Tenant isolation lives in the database.Each request sets the tenant on its connection with
SET LOCALand PostgreSQL row-level security filters every query. A forgottenWHEREcan’t show one company another company’s orders. - The counter keeps working without internet.Kiosk and terminal write every action to an IndexedDB outbox first and replay it when the connection returns. Each entry carries its
Idempotency-Key, so a replay after a timeout can’t hand out a meal twice. - Nobody retypes the menu.The kitchen already writes its weekly menu in Word. A small Python service turns that
.docxinto dishes, days and allergens. Prices come from the dated price list when the menu is published. - Staff accounts are hard to steal.Refresh tokens rotate and are stored hashed; reusing an old one revokes the whole family. Staff can add a TOTP second factor with recovery codes.
- Month-end is a background job.The billing report runs on a BullMQ worker and lands as an XLSX with a notification, instead of holding a request open while it counts a month of meals.
Where it is now
398 commits since April 2026 and 123 test files across Vitest and Playwright. Ordering, dispensing, the kiosk, reports and two-factor sign-in are in place. The current phase is offline resilience; next is a small edge node in each canteen so kiosk and terminal can see each other even when the building loses its connection.
The same backend feeds FirmFlow, the mobile app employees use to order and pick up lunch.