All work

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

Live orders board with pending, preparing and requests columns, above counters for active orders, estimated revenue, items needing attention and pending requests.
The live orders board is the screen the kitchen actually watches. Tickets move between columns as staff act on them, pushed over Socket.io rather than polled — shown here on a quiet account, so the columns are empty.
Menu management screen showing a dish card with price, preparation time, calorie count, stock level and a visibility toggle, above category filters.
Menu items carry the operational data the kitchen needs — prep time, stock level, visibility — not just a price. Hiding a dish is one toggle, which matters when an ingredient runs out mid-service.
QR code management screen showing a generated code for an indoor table with a four-person capacity and a download action.
Each table gets its own QR code, so a scan carries table identity into the order. That is what lets a diner order without an account while the kitchen still knows where the food goes.

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

  1. 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.
  2. 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.
  3. A diner ordering from their own phone is an untrusted client on an unmanaged device, which makes server-side authorisation the only meaningful control.
  4. 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

  1. Clients

    • Diner (QR menu)
    • Floor staff
    • Kitchen display
    • Owner dashboard
  2. Transport

    • REST (transactions)
    • Socket.io (live state)
  3. Application

    • Express MVC
    • JWT / Passport OAuth
    • Payments interface
  4. Data

    • Redis (hot reads, sessions)
    • MongoDB (system of record)
  5. External

    • Stripe
    • Safepay
    • Cloudinary
    • Google Generative AI

Cross-cutting

  • Server-side role checks on every route and socket event
Reads are absorbed by Redis; writes go through Express and fan back out over Socket.io, so every open screen converges on server state rather than its own.
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