Live in production
ChefOS
Restaurant operating system — QR ordering, kitchen display, inventory and payments
- My role
- Sole engineer — real-time architecture, authorisation model, payments, frontend
- Timeline
- 2025 — 2026
- Team
- Solo
Outcome
- Live
- deployed and operating at chefos.pro
- Zero
- paper tickets between counter and pass
- QR
- ordering on the diner's own device — no hardware
- Dual
- domestic and international payment rails
What it looks like



Context
A restaurant service runs on paper tickets and shouted corrections. The counter takes an order, someone carries it to the pass, and stock is reconciled at closing — if at all. The predictable results are duplicated orders, dishes sold after their ingredients ran out, and no dependable picture of the current state of service.
The problem
- Three audiences — diners, floor staff and the kitchen — need three different views of the same live data, and they are all looking at it simultaneously on separate devices.
- Request/response polling is the wrong shape for a kitchen. An order that surfaces thirty seconds late is an order that leaves the pass late.
- A diner ordering from their own phone is an untrusted client on an unmanaged device, which makes server-side authorisation the only meaningful control.
- A restaurant is not a DevOps team. Deployment, secrets and third-party credentials all had to be someone else's problem once installed.
How I approached it
WebSockets for state that changes underneath the user
Order, kitchen and inventory state is pushed over Socket.io rather than polled, with clients subscribing only to the channels their role requires — the Kitchen Display System does not pay to receive billing traffic.
The server remains the single source of truth. Clients render what they are told rather than computing authoritative state locally, which is what keeps several simultaneously open screens from drifting apart during a rush.
Scan-to-order, with the menu as the product surface
Each table carries a QR code resolving to a mobile-first menu, so the diner's own device becomes the ordering terminal and no hardware has to be bought or maintained.
Dishes can carry interactive 3D previews rendered with React Three Fiber. It is the one piece of deliberate spectacle in the product, and it exists because a photograph cannot convey plating the way a rotatable model can.
Google's Generative AI SDK backs dish recommendations, using what is already in the cart and what is actually in stock rather than a static 'popular items' list.
Authorisation on the server, caching in Redis
Sessions are issued as JWTs, with Google OAuth through Passport.js as an alternative sign-in path for staff. Every protected route and socket event re-checks the role server-side; the interface hides what a role cannot do purely as a courtesy.
Redis absorbs the read-heavy paths — menu, stock levels, session lookups — so that a dinner rush does not translate directly into database load.
Two payment processors, because one is not enough locally
Stripe covers international cards; Safepay covers domestic Pakistani rails. Both sit behind one internal payments interface, so the ordering flow neither knows nor cares which processor settles a given transaction.
Subscription billing for the restaurants themselves runs through the same abstraction, which meant the recurring-revenue path did not require a second integration.
Architecture
Clients
- Diner (QR menu)
- Floor staff
- Kitchen display
- Owner dashboard
Transport
- REST (transactions)
- Socket.io (live state)
Application
- Express MVC
- JWT / Passport OAuth
- Payments interface
Data
- Redis (hot reads, sessions)
- MongoDB (system of record)
External
- Stripe
- Safepay
- Cloudinary
- Google Generative AI
Cross-cutting
- Server-side role checks on every route and socket event
- Frontend
- React 19 · Tailwind CSS v4 · React Router v6 · Socket.io client · React Three Fiber
- Backend
- Node.js + Express (MVC) · Helmet · CORS · JWT · Passport.js
- Realtime
- Socket.io channels scoped per role — kitchen, floor, administration
- Data
- MongoDB with Mongoose for persistence · Redis for caching and sessions
- Payments
- Stripe and Safepay behind a single internal payments interface
- Services
- Cloudinary for media · Google Generative AI for recommendations · WhatsApp and voice APIs
- Roles
- Owner/admin · restaurant staff · diner (unauthenticated, QR-scoped)
Trade-offs I chose
WebSockets over polling for the order lifecycle
WhyThe value of a kitchen update decays within seconds. No polling interval is simultaneously cheap enough and fast enough; picking one just chooses which failure to accept.
The server computes state, clients only render it
WhyWith three roles mutating concurrently, any client-side authoritative state guarantees divergence. Pushing computed state trades a little bandwidth for correctness during exactly the period the restaurant cannot afford to be wrong.
Redis in front of MongoDB rather than scaling the database
WhyRestaurant traffic is extremely peaked and overwhelmingly read-heavy. Caching the hot paths addressed the actual load shape at a fraction of the cost of provisioning for peak.
Abstracting two payment processors behind one interface
WhyStripe alone does not serve Pakistani customers well and Safepay alone does not serve international ones. Encoding that choice at the boundary kept it out of the ordering flow entirely.
What I took away
- Real-time is an architectural decision rather than a library choice. Attaching Socket.io to a request/response design would have reproduced every race condition with more moving parts.
- Role-based access is only real when the server enforces it. Every hidden control I added was a convenience and never once a security boundary.
- Integrating a second payment processor was substantially easier than the first, entirely because the first one forced the abstraction into existence.
Stack
- React 19
- Node.js
- Socket.io
- MongoDB
- Redis
- Stripe
- React Three Fiber