
DineGuru — Restaurant POS and Billing System
A restaurant operating system — POS and GST billing, inventory, recipe costing, procurement and tiered analytics on one multi-tenant core.
- Live in production
- Modular monolith
- 299
What it had to solve
A restaurant is not one system, it is a dozen that all reference the same few nouns — an ingredient is a stock level, a line on a recipe, a cost input and a purchase order at the same time. Split those into separate services too early and every question becomes a distributed join; leave them undivided and the codebase becomes one indivisible knot. And because several restaurants share the installation, every one of those queries has to be scoped to a tenant without ever trusting the caller to say which tenant it is.
The decisions that shaped it
A modular monolith, not microservices
Twelve domain packages sit inside one deployment against one database, each with the same internal shape — models, schemas, service, router — and cross-domain access only ever through the service layer, never a model import in a router. The boundaries are real, but recipe costing can still read ingredient prices without a network hop or an eventual-consistency story to explain to anybody.
Tenancy that cannot be passed in
A memberships table is the authorization source of truth, and the active restaurant is resolved from it on every request. The identifier is never accepted as a query parameter or a body field anywhere in the application, and a test greps the call sites to fail the suite if that ever regresses. A tenant boundary the caller gets to name is not a boundary, it is a convention.
Money is recomputed, never received
Subtotal, discount, GST and total are rebuilt from the line items on every mutation; the front end never computes a figure. GST is applied after the discount, which is the correct order for Indian tax treatment, and the rate is snapshotted onto the order at creation so changing a restaurant’s rate can never retroactively rewrite a historical bill.
The draft-to-ticket immutability boundary
Items are added as drafts — billable immediately, freely editable. Firing a kitchen ticket assigns them and makes them immutable; from then on they can only be cancelled, with a reason. Checkout is rejected server-side if any active item is still an unfired draft, so a customer can never be billed for a dish the kitchen was never told to make.
Stock that moves without racing
A stock adjustment is a single arithmetic update under the row’s write lock rather than a read-modify-write, and a negative result is a typed validation error rather than a 500. The weekly stock log posts as one transaction that rolls back entirely on any failure — replacing a per-row loop where a mid-way error left earlier rows committed and a retry double-counted. Ticket-driven deduction locks its ingredient rows and clamps at zero, because a bookkeeping shortfall must never block a kitchen that already holds the physical ticket.
Authentication built from primitives
No external identity provider: bcrypt password hashing with a pre-hash to clear the algorithm’s length cap, stateless short-lived access tokens verified by signature alone, and opaque refresh tokens stored hashed, rotated single-use, and revoked on logout, deactivation or reuse. Authorization is never trusted from the token — the user is re-loaded and the tenant scope re-resolved on every single request. There is no self-signup at all.
How the pieces sit together
- React 18 + TypeScript on Vite; a single fetch wrapper is the only network call site in the app
- FastAPI application factory, 12 domain packages, ~90 endpoints under /api/v1
- models / schemas / service / router per package; business logic and commits live in the service
- PostgreSQL through async SQLAlchemy 2.0 and asyncpg; 25 Alembic revisions on a single linear head
- Resolved per request from memberships; the restaurant id is never client-supplied
- bcrypt hashes, stateless access tokens, rotating hashed single-use refresh tokens
- An httpx client to the delivery platform’s API-key surface; HMAC-signed inbound provisioning and order webhook
- Render against a managed PostgreSQL instance; migrations applied on deploy from two independent layers
Built into the product
POS and billing
Table-based or straight billing, kitchen tickets, discounts and extra charges, cash or UPI checkout, and a bill designer with a live preview.
Inventory
Race-free stock movement, atomic batch adjustment, append-only price history, and a deletion-impact call that shows the real blast radius before you confirm.
Recipe costing
Snapshot and live cost shown side by side with a divergence banner, over a unit-conversion layer shared with ingredients so the two cannot drift.
Procurement
Vendors, ingredient offers with exactly one current supplier per ingredient, and purchase orders that credit stock on receipt.
Analytics
Fifteen endpoints aggregated live at request time — no rollup tables, no staleness — behind a tier gate that maps every widget through one config function.
Rider workspace
A mobile-first surface scoped to the rider’s own deliveries with no tamperable input, carrying handover OTP verification and doorstep QR collection.
What it is built out of
- FastAPI, Python 3.11, Pydantic v2, uvicorn
- PostgreSQL, SQLAlchemy 2.0 async, asyncpg, Alembic
- bcrypt, PyJWT, Fernet
- React 18, TypeScript, Vite, Tailwind CSS
- Radix UI, MUI, Recharts, lucide-react
- pytest, ruff, mypy strict, structlog
What it looks like






What it produced
- 299
- Backend tests across service and integration suites
- 12
- Domain packages in one deployment
- Server-side
- Tenancy resolved from memberships, never from input