Live restaurant operations for a single-premises Kenyan restaurant, driven by WhatsApp ordering and a staff web console. Diners scan a per-table QR code to order over WhatsApp; staff run the floor from role-specific dashboards backed by a realtime Convex database.
- Overview
- Architecture
- Tech stack
- Repository layout
- Features
- Staff roles and access model
- Getting started
- Environment variables
- Seeding data
- Owner PIN recovery
- Common scripts
- Deployment
- Known limitations
Heavenly Foods has two runtime surfaces sharing one Convex backend:
- Staff web console (
apps/web) — a Next.js app where counter, waiter, manager, and owner staff sign in with a PIN and run live operations: the order queue, menu inventory, dining tables and QR codes, staff access, and analytics. - WhatsApp ordering bot (
apps/api) — a Cloudflare Worker that receives WhatsApp webhooks and drives a conversational ordering flow: table binding, menu browsing, cart, checkout, and post-service feedback.
Convex is the single source of truth for restaurants, staff, menu items, tables, orders, WhatsApp sessions, and feedback. There is no separate SQL database.
The design language is a quiet, photography-first, Apple-inspired system: a single blue accent, alternating light/dark surfaces, tight display typography, and Lucide icons. Marketing surfaces use a large editorial scale; operational dashboards use a denser, calmer scale.
WhatsApp (Meta Cloud API)
│ webhook
▼
┌──────────────────────────────────────┐
│ apps/api — Cloudflare Worker (Hono) │
│ signature verify → FSM → Convex │
└───────────────┬──────────────────────┘
│ Convex client
▼
┌──────────────────────────────────────┐
│ packages/convex — data model + │
│ queries / mutations / actions │◄────────┐
└──────────────────────────────────────┘ │ Convex React
▲ │ (realtime)
│ shared types │
┌───────────────┴──────────┐ ┌─────────────┴────────────┐
│ packages/types │ │ apps/web — Next.js 15 │
│ StaffRole, SessionState │ │ RSC session + client UI │
└──────────────────────────┘ └──────────────────────────┘
- Auth is PIN-based. Convex hashes PINs with salted PBKDF2 and issues an HMAC-signed session token; the web app wraps it in an HttpOnly, Secure, SameSite=Lax cookie with a 12-hour lifetime. Middleware guards every dashboard route by role.
- Realtime — the web console subscribes to Convex queries, so order and inventory changes propagate instantly across every signed-in device.
| Area | Choice |
|---|---|
| Monorepo | pnpm workspaces + Turborepo |
| Language | TypeScript (strict, exactOptionalPropertyTypes) |
| Backend / DB | Convex (schema, queries, mutations, actions, file storage) |
| Web | Next.js 15 (App Router, RSC), React 19 |
| Styling | Tailwind CSS + a CSS-variable design system |
| Icons | lucide-react |
| Edge API | Cloudflare Workers + Hono |
| Imports | PapaParse (CSV) and SheetJS (XLSX) |
| QR codes | qrcode |
apps/
api/ Cloudflare Worker — WhatsApp webhook, FSM, message templates
web/ Next.js staff console (counter, waiter, manager, owner)
packages/
convex/ Convex schema and server functions (auth, orders, items, staff, sessions, seed, analytics)
types/ Shared TypeScript types (StaffRole, SessionState, parsed-inventory shapes)
spec.md Product specification and addenda source material
- Role-based staff console — counter, waiter, manager, and owner each get a tailored workspace behind a shared responsive sidebar shell.
- Live order queue — counter staff acknowledge, prepare, ready, serve, and cancel orders; each order carries a short daily reference (for example
0042) for the counter. - Menu inventory with photos — managers create, edit, archive, and toggle availability of menu items. Food images upload directly to Convex file storage; seeded items use credited Unsplash photography with graceful text fallbacks.
- Bulk import — CSV/XLSX menu import with column mapping, per-row validation, and an atomic upsert.
- Tables and QR codes — managers manage dining tables, assign waiters, generate a per-table WhatsApp QR code, export each code as a PNG, and print a full QR sheet.
- Analytics — headline metrics plus an orders-by-hour chart with a 3-hour rollup, top items, table and waiter performance, and lowest-rated items.
- Staff management and audit — owner-aware staff screen with a server-enforced visibility model and an owner-only audit trail of every account change.
- WhatsApp ordering bot — a finite state machine covering greeting, table binding, category and item browsing, cart, name capture, consent, placement, and feedback, with global
menu/cart/cancel/helpcommands.
Four roles form a strict hierarchy. A role may create, modify, or disable only roles strictly below its own level, and never its own record.
| Role | Level | Can manage | Home |
|---|---|---|---|
| Owner | 3 | Manager, Counter, Waiter | /manager |
| Manager | 2 | Counter, Waiter | /manager |
| Counter | 1 | — | /counter |
| Waiter | 1 | — | /waiter |
- The rule is enforced by a single
assertMayManageguard called inside every staff-mutating Convex function — not just hidden in the UI. A manager attempting to create a manager is rejected server-side. - The
/manager/staffscreen applies the same predicate for visibility: a manager never receives owner or manager rows from the server, and the owner-only audit trail rejects non-owner callers. - Every create, role change, enable, disable, and PIN reset writes exactly one
staffAuditrow in the same mutation. Audit rows never contain PIN values or hashes. - Sign-in is PIN-only. The owner shares the
/managerdashboard; there is no separate owner route. Owner PINs must be 6 digits; other roles may use 4 to 6.
Prerequisites: Node.js ≥ 20, pnpm 11, and a Convex account.
pnpm install
# 1. Link the Convex backend (creates/links a dev deployment and generates types)
pnpm --filter @heavenly/convex exec convex dev --once
# 2. Configure the web app environment (see below), then run the console
pnpm --filter @heavenly/web devFor active backend development, run convex dev in a separate terminal so schema and function changes deploy on save. Long-running dev servers should be started manually in your own terminal.
Copy the examples and fill them in. Nothing secret is committed; every key is present and blank in the .example files.
Web (apps/web/.env.local, from .env.example):
| Name | Purpose |
|---|---|
NEXT_PUBLIC_CONVEX_URL |
Convex deployment URL |
SESSION_SECRET |
≥ 32-char secret for signing the staff session cookie (must match Convex) |
NEXT_PUBLIC_RESTAURANT_ID |
The seeded restaurant id |
NEXT_PUBLIC_WHATSAPP_MSISDN |
Restaurant WhatsApp number used to build table QR links |
Worker (apps/api/.dev.vars, from apps/api/.dev.vars.example): WHATSAPP_TOKEN, PHONE_NUMBER_ID, VERIFY_TOKEN, META_APP_SECRET, CONVEX_URL, NVIDIA_API_KEY, RESTAURANT_ID, BUILD_SHA.
Convex deployment variables: SESSION_SECRET (matches the web value) and, only while seeding, a temporary SEED_SECRET.
Seeding is guarded by a SEED_SECRET set on the Convex deployment and is designed to run on an empty deployment, then be enriched. A typical bootstrap:
- Set a temporary
SEED_SECRETon the deployment. - Run
seed:demowith the restaurant name, WhatsApp number, and one PIN per role to create the restaurant, staff, tables, and menu. - Run
seed:enrichDemoto add richer descriptions, realistic orders, sessions, and feedback. - Run
seed:addOwnerto create the single owner account (6-digit PIN). - Remove
SEED_SECRETfrom the deployment.
The seed creates one account per role. PIN values are chosen at seed time and are not stored in this repository; keep them out of version control and rotate them for any non-development use.
There is no in-app path to create an owner or reset the owner PIN — this is a deliberate MVP simplification. If the owner PIN is lost, recovery runs out of band from the Convex dashboard/CLI:
- Set a temporary
SEED_SECRETon the deployment. - Run the
seed:resetOwnerPinaction with the restaurant id and a new 6-digit PIN. - Remove
SEED_SECRET.
This intentionally avoids building an email/SMS account-recovery channel, which is out of scope for a single-premises tool.
Run from the repo root:
pnpm dev # turbo dev across apps
pnpm build # turbo build
pnpm typecheck # tsc --noEmit across the workspace
pnpm lint # lint across the workspace
pnpm test # run package testsPer package, use pnpm --filter @heavenly/web <script>, @heavenly/api, or @heavenly/convex.
- Convex — deploy functions with
convex deploy; setSESSION_SECRETon the production deployment. - Web — deploy
apps/webto a Next.js host (for example Vercel) with the production environment variables. - Worker — deploy
apps/apiwith Wrangler; configure WhatsApp secrets withwrangler secret putand point the Meta webhook at the Worker URL.
- PIN-based, single-premises auth. Adequate for staff with physical access to the till; not built for remote or multi-tenant access.
- Owner PIN has no in-app recovery (see above).
- No payments. Settlement happens at the counter. Any order summary is a record of an order, not a payment receipt or fiscal document.
- Single restaurant. The schema is multi-tenant-shaped but tenant isolation is not enforced end to end.
- Analytics compute live from order and feedback data — fine at demo scale, not for very large histories.
- WhatsApp Flows, OCR menu import, and broadcast messaging are deferred.
- Menu images require manual upload; items without a photo degrade gracefully to text.