Menew
Engineering Dossier

The whole system,
end to end.

A multi-tenant, QR-code restaurant ordering platform — from its origins as Sofra to the hardened, sales-ready product it is today. This document is the map: the story, the architecture, every file, and the questions a developer will ask.

React 19 + Vite Supabase / Postgres Row-Level Security SECURITY DEFINER RPCs Deno Edge Function Vercel PWA / Service Worker EN / AR · RTL
Product
Menew — QR ordering SaaS
Codebase
~5 roles · single SPA · one DB
At time of writing
v74 (in progress)
Audience
Engineers evaluating the project
Contents

How to read this

00

What Menew is

A diner scans a QR code on their table and orders from their phone. Everything else is in service of making that one moment work for the restaurant, the kitchen, the waiter, and the owner.

Menew is a multi-tenant SaaS: one codebase and one database serve many restaurants, each isolated from the others. It is sold to restaurants in Lebanon (and, increasingly, beyond), which shapes a few non-obvious requirements — cash-heavy payment flows, WhatsApp as the default communication channel, full Arabic with right-to-left layout parity, and graceful behaviour on slow or intermittent connections.

There are five kinds of user, and the whole app is organised around them:

One React single-page app renders all five experiences, picking which one to show from the URL. One Supabase project stores everything behind it. That is the entire shape of the system — the rest of this document is detail.

01

From Sofra to Menew

The product wasn't designed all at once — it was discovered through dozens of build-and-harden cycles. This is the honest sequence, because the scars explain the architecture.

ORIGINS · "Sofra"
A menu, then a system

It started as Sofra. The first real work was unglamorous and important: converting two restaurants' HTML menus (Capri and Pablo's — ~193 items across 22 JSON files) into a clean schema, complete with Arabic translations. From there grew a marketing landing page and the core multi-role architecture that still stands today: Master Admin, Restaurant Admin, Kitchen, Waiter, and the Customer menu. Animated logo concepts (the Menu → Menew morph) were explored in parallel.

CUSTOMER MENU
The centrepiece gets its polish

The diner-facing menu received the most design attention: multiple restaurant themes, a hamburger drawer, a category-card grid, dark/light mode, item photos with emoji fallbacks, "pairs well with" recommendations, dietary filter chips, and a service worker for offline caching. Group ordering arrived with a shared countdown and ref-based dispatch to dodge stale-closure bugs.

v71 → v73 · THE RACE CONDITION
Group ordering nearly broke the product

The hardest bug in the project's history. When several phones at one table confirmed within the same second, an in-flight cart-sync write could land on top of the submit and erase the session's submitted status — stranding everyone else in their cart even though the order reached the kitchen. The fix was layered: an atomic, row-locked Postgres function (confirm_seat); a deterministic per-sitting order id every device can compute independently; a seat lock that stops background syncing once a device confirms; and guards that refuse to resurrect a submitted session. Exactly one order per group, guaranteed.

v72 → v73 · THE SECURITY MIGRATION
From "it works" to "it's safe to sell"

A staged lockdown. Phase 1 added the auth/tenancy scaffolding (app_admins, restaurant_members, RLS helper functions). Phase 2 replaced a bundled master password and sessionStorage flags with real Supabase Auth. Later phases moved every public operation behind SECURITY DEFINER functions and locked Row-Level Security across all tables, so the public anon key can no longer read or write anything it shouldn't. A Deno Edge Function was added to provision admin logins server-side, with a forced password change on first login.

nacc-11 → nacc-20 · THE WAITER WAVE
A real staff system

A large, multi-session feature wave. A "Waiter Confirmation" gate parks customer orders at pending until a waiter verifies them. Each waiter became their own person — individual token, PIN, and personal link — so every confirm, edit, void, and table order is attributed server-side and can't be spoofed. The kitchen learned to flash when a waiter edits a live ticket, with a "Got it" acknowledgement. Device tracking arrived so a stolen link stands out. Order-level notes, allergy flags, payment status (cash/card), discounts, refunds, daily sales summaries, and Web-Audio notifications all landed in this era.

nacc-10, nacc-18 → nacc-23 · THE PRE-SALE HARDENING
Mobile sweeps, RTL parity, and the rename

The final push toward sales-readiness: systematic overflow fixes across every page, a premium animated hamburger drawer, delivery/pickup quick orders, "add items to any order" (even completed ones), shared-bill splitting, and a thorough Arabic sweep so RTL admin matches LTR exactly. PII-stripping public RPCs, Web-Crypto-grade randomness, and PIN handling were tightened. And the app was renamed Sofra → Menew across every surface — with internal storage keys deliberately left unchanged to preserve saved carts and preferences.

Why the history matters Two themes recur and explain almost every design decision below: (1) concurrency is hard when many phones share one table, so state is made deterministic and writes are made atomic; and (2) the public anon key is, by design, in every customer's browser — so safety comes from the database, not from hiding the key.
02

Architecture at a glance

One client, one data boundary, one backend. The cleverness lives in the boundary between them.

Five experiences · one SPA
Customer
/menu/<slug>
Kitchen
/kitchen/<token>
Waiter
/waiter/<token>
Resto Admin
/admin/<slug>
Master
/admin
React 19 (Vite) — App.jsx parses the URL & gates auth
App.jsx router + auth gate
path → page · session check · must-change-password
The single data boundary — every read/write goes through here
data.js
Supabase client · all queries · all RPC calls · auth helpers · themes · billing math
Supabase (Postgres) — the backend
Tables + RLS
7 core tables, locked down
SECURITY DEFINER RPCs
~30 vetted operations
Auth
admin email + password
Edge Function
admin-provision (Deno)

The shape in words

The browser runs a React 19 single-page app built with Vite. There is no router library — App.jsx reads window.location.pathname, decides which of the five pages to render, and (for admin pages) verifies the Supabase session before letting anyone in. Navigation is plain history.pushState wrapped in a nav() helper passed down to each page.

Every component talks to the backend through one module: src/data.js. Nothing else imports the Supabase client. That single chokepoint is what makes the security model legible — you can read one file and know every way data can move. It holds the client, all table queries, all RPC wrappers, the auth helpers, and even pure helpers like theme tokens and billing math.

The backend is Supabase — managed Postgres with Row-Level Security, PostgREST for auto-generated REST, Auth for admin logins, and one Deno Edge Function for the privileged operation (creating admin accounts) that must never touch the browser. The app is built to static files and deployed on Vercel from GitHub; vercel.json rewrites all paths to index.html so the client-side router works on hard refreshes.

One deliberate constraint SQL migrations are not in the repo — they're already applied to the live project. Only the two idempotent station-setup scripts (waiter_access.sql, waiter_devices.sql) and the Edge Function ship in /supabase, because those are the pieces a fresh deploy still has to run.
03

The data model

A pragmatic hybrid: relational columns for the things you filter and join on, a JSONB obj for everything else.

The obj pattern

Almost every table follows the same layout — a few real columns for indexing and tenancy (id, restaurant_id, slug, created_at) plus a single JSONB obj column holding the full domain object. The app reads .select('obj') and gets back the exact shape it works with in JavaScript, no mapping layer.

-- e.g. an order row
{ id, restaurant_id, created_at, obj: {
    id, orderCode, restaurantSlug, table, members:[…], total,
    orderType, status, statusHistory:[…], customer?, allergy?, … } }

This is why new fields — orderType, customer, allergy, payment status, discounts — shipped repeatedly without a single schema migration: they live inside obj. The trade-off (and a developer will ask) is discussed in the Q&A.

The tables

TableHoldsTenancy / access
restaurantsPer-tenant config: name, slug, theme, plan, owner PII, billingMembers read full row; public reads a PII-stripped copy via RPC
menu_itemsOne row per dish (obj = name, price, options, allergens, image…)Public read; writes RLS-gated to members
ordersEvery order, with members, items, status historyWrites via vetted RPCs; reads PII-stripped for customers
table_sessionsLive group-ordering state (deterministic id per table)Short-lived; atomic confirm via confirm_seat
logsAppend-only activity trail (who did what, when)Read by members/master; written by RPCs & client
kitchen_accessKitchen's token + pin per restaurantResolved server-side only, never bulk-readable
waiter_access · waiter_staffLegacy shared waiter link; one row per individual waiterToken+PIN resolved inside SECURITY DEFINER funcs

Two more tables support the system: app_admins and restaurant_members back the identity model (who is a master, who administers which restaurant), and access_devices records which browsers have logged into each station for security visibility.

Identity at three levels

04

The security model

The single most important thing to understand about Menew: the public API key sits in every diner's browser, on purpose — and that is fine, because the database refuses to do anything it shouldn't.

Why the anon key is safe

Supabase's anon key is public by design; it identifies the project, it doesn't authorise anything. Real authorisation is enforced by Row-Level Security (RLS) on every table. After the v72–v73 lockdown, the anon role has no blanket read or write on the sensitive tables. So how does an anonymous customer place an order? Not directly — through a vetted gate.

SECURITY DEFINER as the only door

Every operation a non-admin needs is a Postgres function marked SECURITY DEFINER with a pinned search_path. The function runs with elevated rights, but it does exactly one narrow thing and validates its inputs first. Some load-bearing examples:

The bug that proved the model When RLS was first locked down, anonymous orders silently failed — the old code did a direct table insert that RLS (correctly) rejected, but the error was swallowed, so an order "succeeded" and then vanished. The fix was both the new place_customer_order RPC and making the client throw on failure. Lesson baked into the code: writes that can be refused must surface the refusal.

Admins: real accounts, server-side provisioning

Masters and restaurant admins authenticate through Supabase Auth. Creating those accounts is privileged — it needs the service-role key, which must never reach the browser. So account creation lives in the admin-provision Edge Function (Deno), which: verifies the caller is a master using their JWT, then uses the service-role client to create/link the user with must_change_password: true. On first login the app routes them to ForceChangePassword.jsx before the dashboard. The service-role key exists only inside that function's environment.

Smaller, deliberate hardening

05

Every file, explained

What each file is for, and how it connects to the rest. Sizes are a rough sense of weight, not a metric.

Project root — config & deploy
package.json+ package-lock.json
Dependencies and scripts. Lean by design: @supabase/supabase-js (backend), react + react-dom 19, jspdf (receipts & credential PDFs), jsqr (in-browser QR scanning), jszip (Master Admin client exports), and Vite as the build tool. No router, no state library, no UI kit.
vite.config.js
Vite + React plugin. Intentionally minimal.
vercel.json
A single SPA rewrite — every path falls through to index.html so client-side routes survive a hard refresh on Vercel.
index.html
The HTML shell. Notably contains a static splash screen styled with system fonts so it paints in 0 ms on every device before any JS runs; main.jsx removes it once React hydrates. Also preloads the brand fonts.
.gitignore
Standard ignores. Crucially excludes .env — the Supabase URL/anon key live there locally and as Vercel environment variables, never in git.
SETUP.md
Operator quick-start: install, run, env vars, the URL scheme for each role, and the demo restaurant (olys).
CHANGELOG.md
The project's memory. Every release (v70 → v74) with grouped bullets and the migration each one needs. The closest thing to design documentation — read it to understand why things are the way they are.
Build tooling & assets
public/favicon.svg
The Menew "M" mark — a geometric violet (#863BFF) glyph. The brand's accent colour traces back to here.
public/sw.js
The service worker. A per-resource caching strategy (detailed in §08): network-first for the app shell & live data, stale-while-revalidate for menus, cache-first for fonts — tuned for slow Lebanese connections.
menu_import.json
A sample bulk-import payload (22 items, EN+AR, options, allergens) matching the importer's expected shape — the output format of the external menu_to_menew.py HTML-to-JSON converter.
qr-print-designs.html
A standalone preview of 10 table-QR card designs (sized for the NIIMBOT K3 label printer, 70×100 mm) to pick from before wiring the chosen one into the admin's print flow.
src/ — application core
main.jsx
Entry point. Mounts <App/> in React StrictMode, fades out the static splash, and registers the service worker.
App.jsx
The router + auth gate. Parses the path into {page, slug, token, …}, runs the auth check for admin pages (and only re-checks when sign-in state actually flips, not on every token refresh — a hard-won fix against spurious reloads), handles the must-change-password redirect, paints a route-aware base background to kill white flashes, and renders one of the five pages.
data.jsthe data boundary — ~700 lines
The single most important file. The only place the Supabase client is created. Holds: auth helpers; admin provisioning wrappers (calling the Edge Function); kitchen/waiter station RPC wrappers; device-tracking calls; restaurant/menu/order/log CRUD; the public PII-stripped reads; the entire table-session group-ordering engine (join, sync, atomic confirm, deterministic order id); billing math; image compression; and shared constants — themes, plan feature flags, crypto-random id generators. If you read one file, read this one.
i18n.js~435 keys
A tiny hand-rolled bilingual engine. Module-level language state, an {en, ar} dictionary, a t(key) lookup, a useLang() hook that re-renders subscribers on switch, and the RTL flip (document.dir). Also exports OPT_AR and translateNote for translating menu option strings on the fly.
index.css~660 lines
Global styles: reset, keyframes, the customer-menu theme variables (driven per-theme/mode by React), the admin shell, the responsive grids, the mobile top bar + animated hamburger drawer, and the RTL overrides. The CHANGELOG's mobile fixes are mostly edits here — including the subtle source-order and specificity bugs that hid the mobile nav.
notify.js
Notifications without asset files: a synthesized Web-Audio chime (used by kitchen/waiter/customer) plus desktop/mobile push notifications. Allergy orders get a distinct triple low tone.
useBackButton.js
A history-guard hook (useOverlayBackButton) that makes the Android hardware Back button close the topmost open overlay instead of leaving the app — and correctly distinguishes a Back-close from a ✕/backdrop close so it never strands dead history entries. Wired into the menu (12 layered overlays), and both admin pages.
adminTheme.js
Two admin-shell themes (Light "ocean" / Dark "midnight") as CSS-variable maps, persisted in localStorage. Separate from the customer-facing menu themes.
LangToggle.jsx
The EN ⇄ ع sliding toggle used across every surface. Pure presentational, backed by i18n.js.
ItemCustomize.jsx
The shared item-customization bottom sheet (size/options/extras + free note + qty) and the price math (calcOptPrice, buildOptNote). Reused by the customer menu, the admin quick-order, and the waiter station so a "Large" bills identically everywhere.
ForceChangePassword.jsx
The first-login screen shown while an account still has must_change_password. Gates the dashboard until a new password is set.
src/pages/ — the five experiences
Menu.jsxcustomer · ~3,000 lines
The diner app, and the largest file. Live QR scanner, themed/dark-light menu, cart, the group/table-session engine (3 s polling, countdown, atomic confirm, deterministic ids), live order tracking with a bulletproof elapsed timer, the bill + tip + split-by-person/even calculator, favorites, smart recommendations, dietary filters, PDF receipts, and WhatsApp sharing. Everything a customer touches.
RestaurantAdmin.jsxowner · ~3,150 lines
The owner's dashboard. Dashboard analytics + today's summary, orders (with discount/refund/mark-done/add-items/payment status), menu CRUD + bulk import, QR-code generation & print, activity log with staff/action filters, and a tabbed Settings (Account · Kitchen · Waiter) covering kitchen access, the waiter roster, themes, hours, and menu scheduling. Includes a 2-hour inactivity auto-logout.
MasterAdmin.jsxplatform · ~1,440 lines
Menew's own console. Onboard a restaurant (auto-generates slug, admin login, kitchen PIN; shares credentials as a PDF via WhatsApp/email), edit any client, a billing view (MRR/ARR, overdue/due-soon, record payments, optional Google-Sheets sync), and a per-client "Devices with Access" security panel.
Kitchen.jsx~745 lines
The live order screen. Token+PIN login, polling, status advancement, sold-out toggles, the elapsed/prep timer (with the famous NaNh NaNm fix), the red ALLERGY banner, delivery/pickup badges, and the amber "changes by <waiter>" flash with a Got-it acknowledgement.
Waiter.jsx~935 lines
The confirm-before-kitchen station. Per-waiter token+PIN, "Awaiting review" vs "In the kitchen" sections, full order editing (add/remove people, add shared or per-person items, notes, qty), Confirm & Send / Save / Void, a from-scratch table order, live kitchen-status chips, and chimes for orders needing review.
Login.jsx
The admin sign-in screen (master & restaurant variants), with the email + password flow and password-reset request.
supabase/ — the backend that ships
README.md
Which SQL to run and how to deploy the Edge Function. Explains why migrations aren't in the repo.
waiter_access.sql
Idempotent, dependency-free setup for the waiter system: the waiter_access + waiter_staff tables and the full set of SECURITY DEFINER station functions (login, orders, update, set-status, create-order, per-waiter management, kitchen-ack). Uses core md5/random rather than pgcrypto — a deliberate fix after pgcrypto turned out to be invisible to a search_path=public function.
waiter_devices.sql
Idempotent device-tracking setup (the access_devices table + record/list/forget functions for waiter, kitchen, and admin logins). Depends on waiter_access.sql.
functions/admin-provision/index.ts
The Deno Edge Function. The only code that holds the service-role key. Verifies the caller is a master, then creates/links admin logins or resets passwords — always with must_change_password set.
One cleanup worth naming There is a stale duplicate src/Menu.jsx alongside the live src/pages/Menu.jsx. App.jsx imports the pages/ one; the root copy is dead and should be deleted to avoid confusion. (Flagged here because an evaluating developer will notice two 3,000-line files with the same name.)
06

Four flows that matter

The walkthroughs that show how the pieces connect under load.

① A customer places a solo order

  1. Customer opens /menu/<slug>. Menu.jsx calls getPublicRestaurant(slug) → the public_restaurant RPC returns the restaurant without owner PII.
  2. They build a cart (prices computed by the shared ItemCustomize math) and confirm.
  3. placeOrder() in data.js builds the order object, generates a crypto-random orderCode, and calls place_customer_order(slug, obj).
  4. The RPC (running as definer) resolves restaurant_id from the slug, clamps the status, inserts the row, and returns. If the waiter gate is on, status is parked at pending; otherwise new.
  5. The customer polls track_order(id, orderCode) for live status; the kitchen polls kitchen_orders(token, pin) and advances it.

② A group orders together (the hard one)

  1. Each phone scanning the same table computes the same deterministic session id (ts_<restaurant>_t<table>) and joins one shared table_sessions row.
  2. Each device debounces its cart into the session every 800 ms; all devices poll the session every 3 s to see each other's carts.
  3. When everyone confirms, confirmSeatAtomic() calls the row-locked confirm_seat function, which marks the seat confirmed and — if all are now confirmed — designates exactly one submitter.
  4. The order id is derived deterministically from the session (o_<session>_<createdAt>), so even if the ephemeral session record is clobbered or expires, every device can still find the placed order directly. No phone is ever stranded.
  5. Once a device confirms, its seat locks: background cart-sync stops, removing the write that used to cause the race.

③ A waiter confirms before the kitchen sees it

  1. With Waiter Mode on, customer orders land at pending — invisible to the kitchen.
  2. The waiter opens their personal /waiter/<token>, enters their PIN; waiter_login resolves who they are from the token.
  3. They edit freely (the 9 s auto-refresh never clobbers local edits), then Confirm & Send → waiter_set_status(..., 'new') releases it to the kitchen, attributed to them server-side.
  4. If they later edit a ticket the kitchen already has, the RPC stamps kitchenNeedsAck; the kitchen card flashes amber until a cook taps "Got it" (kitchen_ack_changes).

④ The master onboards a restaurant

  1. Master fills the onboarding form. onboardRestaurant() creates the restaurant row and a crypto-random admin password + kitchen PIN (returned for display only — never stored in the DB).
  2. To create the actual login, the client calls the admin-provision Edge Function, which verifies the caller is a master, then uses the service-role key to create the Auth user with must_change_password: true and link them in restaurant_members.
  3. Credentials are shared as a polished PDF via the device's native share sheet (WhatsApp/email). On first login the new admin hits ForceChangePassword before reaching their dashboard.
07

Bilingual & RTL

Arabic isn't a translation pass bolted on at the end — it's a structural requirement, and the layout has to mirror, not just relabel.

The engine in i18n.js is deliberately tiny: a key-based {en, ar} dictionary (~435 entries), a t() lookup that reads the current language at call time (no stale closures), and a useLang() hook so any component re-renders the instant the language flips. Switching also sets document.documentElement.dir to rtl, and index.css carries the mirrored layout rules.

RTL parity is where the real bugs lived. The CHANGELOG's nacc-23 fix is illustrative: an RTL sidebar-offset rule had higher specificity than the mobile reset, so on phones the entire Arabic admin got crammed into a ~250 px column while English was fine. The lesson — every RTL override must be scoped to the breakpoint where the element it compensates for actually exists. Menu option strings (sizes, extras) are translated on the fly via OPT_AR / translateNote, so a kitchen ticket reads correctly in either language.

08

Offline & Lebanon resilience

Slow Wi-Fi and power cuts are the operating environment, not the edge case. The caching strategy is shaped around that.

The service worker (public/sw.js) routes each request by what it is:

Beyond caching, the Lebanon context drives product choices visible throughout: cash/card payment tracking with a card-toggle for cash-only venues, WhatsApp as the share channel for bills and credentials, and a static 0-ms splash so a slow first paint never shows a blank screen. A premium "local-LAN node" architecture for full offline operation has been scoped as a future tier.

09

Plans, billing & tenancy

The commercial layer is data, not branching logic scattered through the UI.

A single source of truth — PLAN_FEATURES in data.js — maps each tier to feature flags (ordering, table sessions, group ordering, kitchen access, order history, multi-QR print). Components call getPlanFeatures(restaurant.plan) rather than checking plan names inline, so a Basic menu-only restaurant simply has the cart and kitchen switched off, while Starter / Pro / Premium unlock the full system. Four tiers are defined with monthly prices and onboarding fees, plus yearly-discount math.

Master Admin's billing view computes MRR/ARR across all clients, flags overdue and due-soon accounts, records payments into a per-restaurant history, and can optionally mirror each payment to a Google Sheets webhook. Tenancy itself is enforced one level down — by RLS and the restaurant_id stamped server-side on every write — so the plan layer is purely about what features a paying tenant sees.

10

Developer Q&A

The questions a sharp engineer asks in the first hour. Answered straight — including where the answer is "that's known tech debt."

Q.Why a JSONB obj column instead of a normalised relational schema?

Speed of iteration for a solo founder. The domain shape changed constantly — orderType, customer, allergy flags, payment status, discounts all arrived after launch — and every one shipped without a migration because it lives inside obj. The columns that actually need indexing or tenancy enforcement (id, restaurant_id, slug, created_at) are real columns; everything else is the document. The trade-off is honest: you can't easily do SQL aggregate queries across order line-items, and you rely on application code to keep obj well-formed. For this product's scale and pace, that's the right trade — and the relational columns are exactly the ones you'd want if a reporting layer were added later.

Q.The Supabase anon key is shipped in the browser. Isn't that a vulnerability?

No — it's how Supabase is designed to work. The anon key identifies the project; it authorises nothing on its own. Authorisation is Row-Level Security on every table plus SECURITY DEFINER functions as the only door for non-admin operations. The anon role can't read owner PII, can't read another diner's order details, and can't write an order except through place_customer_order, which stamps tenancy server-side. Treat the key like a public URL, not a password.

Q.How is one restaurant's data isolated from another's?

By restaurant_id + RLS. Admin reads are gated to members via is_restaurant_member(); the master is checked via is_app_admin(). Critically, the client never gets to assert which restaurant a write belongs to — the definer functions resolve restaurant_id from a slug or a station token and stamp it themselves, so a customer or waiter can't write into a tenant they don't belong to.

Q.How do the kitchen and waiter authenticate with no user accounts?

An unguessable token (in the URL) plus a PIN, both validated inside the database function. There's no Supabase Auth account and no JWT — the token+PIN resolve to a restaurant_id (and, for an individual waiter, their name) within the SECURITY DEFINER function. A wrong PIN resolves to nothing and the action no-ops. Links can be rotated instantly if one leaks, and device tracking surfaces unfamiliar logins.

Q.What stops a stranger from reading someone else's order by guessing the id?

Two things. The customer-facing reads (track_order, find_table_order) require the secret orderCode — a crypto-random bearer token — to match, and they return the order with the customer block (name/phone/address) stripped server-side. So even a correct id with the right code never exposes another diner's contact details, and a wrong code returns nothing.

Q.Walk me through the group-ordering race. How is it actually solved?

Three mechanisms working together. (1) Determinism: the order id is derived from the session, so every device can compute and find the same order independently of the fragile session record. (2) Atomicity: confirmation runs through the row-locked confirm_seat Postgres function, so concurrent confirms can't overwrite each other, and exactly one submitter is chosen. (3) Seat locking: once a device confirms, its background cart-sync stops — that sync write was the thing landing on top of the submit and erasing it. Plus defensive guards so a late write can never resurrect a submitted session.

Q.Do you use Supabase Realtime, or polling?

Polling — 3 s for table sessions, ~8–9 s for orders/logs. It's deliberately simple and robust on flaky connections (a dropped poll just retries), and it composes cleanly with the service worker's network-first-with-timeout fallback. Realtime websockets would cut latency and server reads, and it's a reasonable future optimization; polling was chosen for predictability under the target network conditions.

Q.Menu images are base64 inside JSONB. Why, and isn't that heavy?

It's a zero-config trade-off: images are compressed client-side to ~50–80 KB WebP and stored inline, so there's no Storage bucket to provision or secure. It keeps setup trivial, which matters for a solo operator onboarding small restaurants. The cost is a larger menu payload — which is exactly why menus are cached stale-while-revalidate. Migrating images to Supabase Storage URLs is a noted follow-up to shrink the payload on slow connections; the image field already accepts a URL, so the path is clear.

Q.Saving the menu does a delete-then-insert of all items. Concurrency risk?

Yes, that's a known sharp edge. saveMenuItems deletes the restaurant's items and re-inserts the list, which is simple but not safe against two admins editing simultaneously, and it's not atomic. In practice a restaurant has one admin editing the menu, so it hasn't bitten — but it's the kind of thing to wrap in a transaction or move to per-item upserts before multi-admin menu editing is a real scenario. Per-item add/edit/toggle/delete already use targeted upserts; only the bulk save is delete-then-insert.

Q.No router, no Redux, no UI library. Why hand-roll everything?

Bundle size and control on slow connections, plus the routing need is genuinely tiny — five top-level pages keyed off the path. A router library would add weight for a switch statement. State is local to each page with refs used carefully where async closures would otherwise capture stale values (the polling loops). UI is hand-built so the theming system and RTL can be exact. It's more code, but it's code you can read end-to-end, which for a solo-maintained, about-to-be-sold project is a feature.

Q.What's the automated-testing story?

Honest answer: there's no automated test suite. Quality has been maintained through disciplined manual testing, a meticulous CHANGELOG that records every fix and its root cause, and defensive coding (NaN guards, throw-on-failure writes, idempotent SQL). That's the clearest area for investment if a team takes this on — the high-value targets are the pricing/option math in ItemCustomize, the group-order state machine, and the definer-function authorization paths, all of which are pure-ish and testable.

Q.How are secrets managed?

The Supabase URL and anon key are public-by-design and injected as Vite env vars (VITE_*) at build time; .env is gitignored and set in Vercel. The only true secret — the service-role key — exists exclusively inside the admin-provision Edge Function's environment on Supabase's servers and is never bundled into the client. No secret is ever placed in a URL or query string.

Q.Where are the SQL migrations? The repo only has two SQL files.

The core schema migrations were applied directly to the live Supabase project and aren't versioned in the repo (a known gap for reproducibility). The two SQL files that are included — waiter_access.sql and waiter_devices.sql — are idempotent, self-contained station/device setup that a fresh deploy still needs to run, and they're written to be safely re-runnable. Formalising the full schema into a supabase/migrations directory is the right next step for a team.

Q.Why md5/random in the SQL instead of pgcrypto?

A real bug, not laziness. On Supabase, pgcrypto lives in the extensions schema, invisible to a function pinned to search_path=public — so gen_random_bytes failed at runtime even though the function compiled. The setup scripts use core md5/random/clock_timestamp helpers so they run on any database with no extension dependency. Note this is for non-credential tokens; user-facing credentials and order codes are generated client-side with the Web Crypto API.

Q.What are the scaling limits as written?

The realistic ceilings: polling generates steady read load that scales with concurrent diners (Realtime would relieve it); base64 images inflate menu payloads (Storage URLs would relieve it); and the bulk menu delete-then-insert isn't concurrency-safe. None are problems at one-restaurant or even town-scale; all have a clear, already-identified remediation. The multi-tenant data model and RLS isolation themselves scale fine — that's Postgres doing what it's good at.

Q.I'm a new developer. Where do I start?

Read CHANGELOG.md top to bottom for the why, then data.js for the how — it's the whole backend surface in one file. Then App.jsx to see routing/auth, and pick the one page for the role you're working on. Run it against the demo restaurant (olys) per SETUP.md. The mental model that unlocks everything: the client is untrusted; the database enforces the rules; data.js is the only bridge.

A

Appendix

Quick-reference catalogs for orientation.

Routes

URLRendersAuth
/menu/<slug>Customer menuNone (anonymous)
/menu/<slug>/t/<n>Customer menu, table presetNone
/kitchen/<token>Kitchen displayToken + PIN
/waiter/<token>Waiter stationToken + PIN (per waiter)
/admin/<slug>Restaurant AdminSupabase Auth (member)
/admin  ·  /Master AdminSupabase Auth (master)

RPC catalog (SECURITY DEFINER)

GroupFunctions
Identityis_app_admin, is_restaurant_member
Public readspublic_restaurant, track_order, find_table_order
Customer writeplace_customer_order
Group orderingconfirm_seat
Kitchenkitchen_login, kitchen_orders, kitchen_set_status, kitchen_toggle_item, kitchen_ack_changes, set_kitchen_access
Waiter — stationwaiter_login, waiter_orders, waiter_update_order, waiter_set_status, waiter_create_order, set_waiter_access
Waiter — staff mgmtlist_waiter_staff, add_waiter_staff, reset_waiter_staff_pin, update_waiter_staff, delete_waiter_staff
Device trackingwaiter_record_device, kitchen_record_device, admin_record_device, list_access_devices, forget_access_device

Environment variables

VariableWhereSecret?
VITE_SUPABASE_URLBuild-time (Vercel / .env)No — public
VITE_SUPABASE_ANON_KEYBuild-time (Vercel / .env)No — public by design
SUPABASE_SERVICE_ROLE_KEYEdge Function env onlyYes — never in client

Glossary

TermMeaning
RLSRow-Level Security — Postgres policies that decide, per row, who can read/write. The backbone of tenant isolation.
SECURITY DEFINERA Postgres function that runs with its creator's privileges — the vetted "door" through which untrusted clients perform narrow operations.
objThe JSONB column holding the full domain object on most tables.
orderCodeA crypto-random per-order secret the customer must present to read their own order.
Waiter ModeThe optional gate that parks customer orders at pending until a waiter confirms.
StationA kitchen or waiter screen authenticated by token + PIN rather than a user account.
MasterThe operator of the Menew platform itself (vs. a restaurant admin).
A closing note This dossier reflects the codebase at v74. The CHANGELOG is the living record; when in doubt, it wins. Everything here is written to be handed to an engineer cold — if a section left you with a question it didn't answer, that's a gap worth closing in the next revision.