Multi-tenant online ordering for restaurants. Each restaurant is a tenant with its own branded storefront, a kitchen screen for live orders, and a dashboard for its menu, outlets and staff, and its data is isolated from every other restaurant's.
Biscotti is a rebuild of the original app (a Firebase project I built as a student in 2020). The goal is a small, correct MVP: prices computed on the server, payments verified, permissions checked on every staff action, and nothing in the database exposed by default.
Live demo · System design · How it works · Rebuild notes · Decisions
Browser (customer, kitchen tablet, owner)
│ pages and Server Actions live order updates
▼ ▲
apps/web (Next.js on Vercel) │
│ thin actions: who is signed in │
▼ │
packages/core (business logic) │
│ validate input, check the role, │
│ price orders, move statuses, payments │
▼ │
packages/db (Prisma) ──▶ Supabase Postgres ─┘ Realtime (row level security decides who hears what)
Supabase Auth (phone OTP), Storage (photos)
Razorpay ──▶ webhook /api/webhooks/razorpay ──▶ core (verify, record payment)
- The browser never sends prices or statuses: it sends ids and quantities, and the server does the rest.
- Every change to restaurant data goes through core, which checks the person's role at that restaurant (owner, manager or staff).
- The database is locked to browsers by default; the only reads it serves directly are live order updates.
What's worth a look:
- Money you can trust: prices come only from the server, amounts are integer paise, an idempotency key stops double orders, and payments are confirmed twice (browser callback and Razorpay webhook) without double counting.
- Tenant isolation, tested: roles are checked per restaurant on every change, every id is checked to belong to that restaurant, and cross-restaurant attacks were tried against the code and refused. The database is locked to browsers by default.
- Live kitchen: a new order reaches the kitchen screen in about half a second (Supabase Realtime, with polling as a fallback).
- One codebase, many brands: each restaurant picks a look and colours, stored as data and turned into CSS variables.
- Written down: a system design in interview format, 11 short decision records, and a backlog of what is deliberately not built yet.
Live demo: https://biscotti-sigma.vercel.app
Sign in with one of the demo phone numbers below and its code (no SMS is sent; these are test numbers). Online payments run in Razorpay test mode: pay with the card 4100 2800 0000 1007, any future expiry, any CVV, and any 4-10 digit OTP. No real money moves.
| Phone | Role | Code | Try |
|---|---|---|---|
| 99999 00001 | Customer | 098765 |
Order from Casa Spezia |
| 99999 00002 | Casa Spezia staff | 098765 |
Kitchen only (the dashboard says "no access") |
A five-minute walkthrough:
- Open Casa Spezia, add a few dishes, open the cart and go to checkout.
- Sign in as the customer (
99999 00001, code098765). Pick delivery (drop the pin inside the circle) or pickup, and pay online with the test card or choose cash. - In a second browser or a private window, sign in as the kitchen staff (
99999 00002, code098765) and open the kitchen. The order is there within a second; press Accept, then move it along. - Watch the customer's order page update by itself.
- The dashboard (menu, photos, outlets, staff, refunds) is shown in the screenshots above; the system design and how it works describe it.
The demo data is shared by everyone who tries it, so it may look a little used.
- Next.js 16 (App Router), React 19, Tailwind CSS 4, shadcn/ui
- Supabase (Postgres, Auth, Storage, Realtime), Prisma
- Razorpay for payments
- pnpm workspaces + Turborepo, tool versions pinned with mise
apps/web Next.js app (storefront, kitchen, dashboard)
packages/core business logic (no Next.js or React imports)
packages/db Prisma schema and client
packages/ui shared UI components (shadcn/ui)
packages/eslint-config shared lint config
packages/typescript-config shared tsconfig presets
docs/ ADRs, architecture notes, backlog
Requires mise (for Node and pnpm) and Docker Desktop (for the local Supabase stack).
mise install
pnpm install
pnpm db:start # Postgres, Auth, Storage, Realtime in Docker
cp .env.example .env
pnpm db:status # copy the publishable and secret keys into .env
pnpm db:reset # wipe the local database, apply migrations, seed demo data
pnpm check # run the order and payment checks against the local database
pnpm dev # http://localhost:3000- Supabase Studio: http://127.0.0.1:54323
pnpm db:stopstops the containers.
pnpm db:reset seeds two restaurants and demo users. Phone login uses fixed test numbers and sends no SMS.
| Phone | User | Code |
|---|---|---|
| +91 99999 00001 | Customer | 098765 |
| +91 99999 00002 | Casa Spezia staff | 098765 |
| URL | Who | What |
|---|---|---|
/ |
anyone | Demo restaurants |
/casa-spezia |
anyone | Storefront: menu, dish details, cart |
/casa-spezia/checkout |
signed in | Delivery or pickup, map pin, cash or Razorpay (test mode) |
/casa-spezia/orders |
signed in | Your orders; each order page updates live |
/casa-spezia/kitchen |
staff, manager, owner | Live order board |
/casa-spezia/dashboard/orders |
manager, owner | Orders, refunds to handle |
/casa-spezia/dashboard/menu |
manager, owner | Categories, dishes, photos, sold out |
/casa-spezia/dashboard/outlets |
manager, owner | Address, map, delivery area, hours, fees |
/casa-spezia/dashboard/staff |
owner | Add people by phone, roles |
/casa-spezia/dashboard/settings |
owner | Name, look, photos, legal pages, GSTIN/FSSAI |
/login, /account |
anyone / signed in | Phone sign-in; your account and restaurants |
Every staff page and action checks the role at that restaurant. Visitors are sent to /login and back; users without the role see a "no access" message.
Online payments use Razorpay test mode: pay with the Indian test card 4100 2800 0000 1007 (any future expiry, any CVV, any 4-10 digit OTP).
The schema lives in packages/db/prisma/schema.prisma. To change it:
pnpm --filter @workspace/db db:migrate:dev --name <change>Things Prisma's schema cannot express (row level security, policies, triggers, check constraints) go into a migration created with --create-only and edited by hand. Every new table needs enable row level security.
Other commands:
pnpm typecheck
pnpm lint
pnpm build
pnpm formatAdd a shadcn component to the shared UI package:
pnpm dlx shadcn@latest add <component> -c apps/web- How it works: the pieces in plain words: frontend and backend parts, three journeys step by step, every table, multi-tenancy, who can do what, and a glossary
- System design: the interview version (RADIO): requirements and estimates, architecture, data model with keys and indexes, interfaces, deep dives and a scaling path
- Rebuild notes: what the original app got wrong, how Biscotti handles each problem, and the bugs the rebuild itself ran into
- Decisions (ADRs): one page per important decision, with its trade-offs
- Backlog: what is deliberately not built yet, in rough priority order




