DineGuru — Multi-tenant SaaS

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.

Status
Live in production
Shape
Modular monolith
Backend tests
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

Client
React 18 + TypeScript on Vite; a single fetch wrapper is the only network call site in the app
API
FastAPI application factory, 12 domain packages, ~90 endpoints under /api/v1
Domain
models / schemas / service / router per package; business logic and commits live in the service
Data
PostgreSQL through async SQLAlchemy 2.0 and asyncpg; 25 Alembic revisions on a single linear head
Tenancy
Resolved per request from memberships; the restaurant id is never client-supplied
Auth
bcrypt hashes, stateless access tokens, rotating hashed single-use refresh tokens
Integration
An httpx client to the delivery platform’s API-key surface; HMAC-signed inbound provisioning and order webhook
Delivery
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

API
FastAPI, Python 3.11, Pydantic v2, uvicorn
Data
PostgreSQL, SQLAlchemy 2.0 async, asyncpg, Alembic
Auth & crypto
bcrypt, PyJWT, Fernet
Client
React 18, TypeScript, Vite, Tailwind CSS
UI
Radix UI, MUI, Recharts, lucide-react
Quality
pytest, ruff, mypy strict, structlog

What it looks like

DineGuru — The till itself — two screens and a card reader on one counter.
The till itself — two screens and a card reader on one counter.
DineGuru — Opening the till at the start of service.
Opening the till at the start of service.
DineGuru — Inventory — stock value, live items and price spikes across the catalogue.
Inventory — stock value, live items and price spikes across the catalogue.
DineGuru — The Saturdays channel: store status, prep time and delivery pricing in one panel.
The Saturdays channel: store status, prep time and delivery pricing in one panel.
DineGuru — Incoming orders on the expediting screen, where the tickets are worked.
Incoming orders on the expediting screen, where the tickets are worked.
DineGuru — The kitchen the twelve domains ultimately describe.
The kitchen the twelve domains ultimately describe.

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