Multi-hotel restaurant management app: customers scan a QR code at a table, browse a menu, order, and request payment; the kitchen tracks orders in real time; the hotel admin manages dishes, tables, offers, loyalty programs, and analytics.
- Backend: FastAPI + SQLAlchemy (SQLite — a single local
Tabble.db) + Firebase Admin - Frontend: React 18 (Create React App 5) + MUI v5 + Redux Toolkit + React Query + Firebase JS
- Deployment: Render (backend) via
backend/render.yaml; auto-deploys from thetabblefinal-backendbranch
Two independent apps — no root package.json / monorepo tooling.
| Path | Purpose |
|---|---|
backend/ |
FastAPI app. Entrypoint app/main.py, 12 routers in app/routers/, app/database.py (SQLAlchemy models + session manager), app/storage_adapter.py (local image storage), app/models/ (Pydantic schemas), app/utils/, app/services/, app/middleware/. |
frontend/ |
React SPA. src/App.js (providers + all routes), src/pages/{customer,chef,admin,analysis}, src/components/, src/services/api.js, src/store/ (Redux), src/hooks/, src/utils/. |
backend/hotels.csv |
Seeds hotels (hotel_name,password) on the first DB creation. |
backend/render.yaml |
Render deploy config (branch: tabblefinal-backend). |
cd backend
.\.venv\Scripts\python.exe run.py # serves on 0.0.0.0:8001 with auto-reload- The venv at
backend/.venvis prebuilt; install once withpip install -r requirements.txt. - Alternative:
.\.venv\Scripts\python.exe -m app.mainserves onPORT(default 8000). - API docs:
http://localhost:8001/docs. backend/.envis not loaded by the code — there is noload_dotenv. Set env vars in your shell/run config, or the in-code defaults apply (see Environment below).- Run from
backend/when you want image uploads to work (storage_adapter.pyuses CWD-relativeapp/static/images/...).
cd backend
pip install pytest # once — the only test dependency
.\.venv\Scripts\python.exe -m pytest50 tests in backend/tests/: unit (order_utils business logic), smoke (boot + seed + auth), integration (the full scan → order → chef per-dish accept/reject → bill → paid flow over the API), and regression (pins every previously-fixed bug). Each test runs against its own fresh temp SQLite DB — Tabble.db is never touched.
cd frontend
npm start # dev server on 0.0.0.0:3000 (reachable from the LAN by default)
npm run build # production build into build/- No root package.json; run npm commands from
frontend/.
| Variable | Default | Used for |
|---|---|---|
DEMO_MODE |
true |
Seeds demo hotel (demo/demo123), 6 dishes, 3 tables×2 slots |
CORS_ORIGINS |
dev origins + auto http://<lan-ip>:3000/:8001 |
CORS allow-list (comma-separated) |
ADMIN_PASSWORD |
adminoftabble |
Super admin login at /adminofthetabble |
FRONTEND_URL |
auto-detected http://<lan-ip>:3000 |
Base URL embedded in table QR codes |
FRONTEND_PORT |
3000 |
Port used when auto-detecting the QR frontend URL |
POC_MAX_TABLES_PER_HOTEL |
1000 |
Soft cap on physical tables per hotel (effectively unlimited) |
FIREBASE_SERVICE_ACCOUNT_JSON / FIREBASE_SERVICE_ACCOUNT_BASE64 |
— | Firebase Admin credentials (Google sign-in verification) |
FIREBASE_PROJECT_ID |
tabble-v4 |
Firebase project |
HOST / PORT |
0.0.0.0 / 8000 |
Only read by python -m app.main (run.py hardcodes 8001) |
Note: RENDER, APP_ENV, DATABASE_TYPE, SECRET_KEY appear in render.yaml but no code reads them. The app is always SQLite.
REACT_APP_API_BASE_URL— API base URL. Dev ishttp://localhost:8001(seefrontend/.env, which is tracked). The runtime resolversrc/utils/apiBaseUrl.jsoverrides it withhttp://<same-host>:8001when the page is opened from a LAN address (phone on the same WiFi).REACT_APP_API_PORT(default8001) changes that port.REACT_APP_FIREBASE_*(API_KEY,AUTH_DOMAIN,PROJECT_ID,STORAGE_BUCKET,MESSAGING_SENDER_ID,APP_ID,MEASUREMENT_ID) — Firebase web-app config; hardcoded fallback keys already exist insrc/firebase.js.
- Hotels are seeded from
backend/hotels.csv(columnshotel_name,password,hotel_id;hotel_idis ignored) only when the Hotel table is empty:tabble_new/myhotel,Hotel_Anifa-Trichy/Anifa@123,user/password - With
DEMO_MODE=true(default) ademo/demo123hotel with dishes and QR-enabled tables is seeded too. - Super admin:
/adminofthetabble, passwordADMIN_PASSWORD. - Chef: hotel username/password via
/chef/auth/login(not Firebase). Admin: hotel credentials. Customer: Firebase Google sign-in.
/Home ·/order?t={token}QR landing ·/customer//customer/menuordering/chef/login,/chef,/chef/orders— kitchen/admin/login,/admin,/admin/dishes|offers|specials|tables|settings|chefs|loyalty|completed-orders|selection-offers— hotel admin/analysis,/analysis/customer|dish|chef— analytics/adminofthetabble— super admin ·/backitup,/sysdiag,/emergency-sys— system monitors
| Prefix | Notable endpoints |
|---|---|
/public |
GET /public/scan/{token} — QR token → hotel/table/slot; GET /public/hotels |
/tables |
POST /tables/, POST /tables/batch, `PUT /tables/{id}/occupy |
/settings |
GET /settings/hotels, POST /settings/switch-hotel, public show-prices |
/customer |
POST /customer/api/login, POST /customer/api/orders, GET /customer/api/person/{id}/orders, `PUT /customer/api/orders/{id}/payment |
/chef |
POST /chef/auth/login, `GET /chef/orders/pending |
/admin |
orders, bills (/admin/orders/{id}/bill), dishes/offers CRUD, /admin/super/auth, /admin/super/hotels (and /stats, /{id}/stats) |
| Others | /feedback, /loyalty, /selection-offers, /analytics, /monitoring |
Full docs: http://localhost:8001/docs.
- Admin generates a QR per table slot (
POST /tables/{id}/generate-qr). The token (uuid4) is created once and stored on the row; the PNG embeds{frontend_url}/order?t={token}(seeFRONTEND_URL). - Customer scans → opens
/order?t=...→QRLanding.jscallsGET /public/scan/{token}→ storescustomerQrToken,customerSelectedDatabase,tableNumber,slotNumberin localStorage → navigates to/customer/menu?....Menu.jsmarks the slot occupied (PUT /tables/number/{n}/occupy?slot_number=...) if it's still free. Menu.jsloads data via theuseMenuOptimizedhooks; the cart is persisted in localStorage ascustomerCart_<qrToken>. Rejected andpayment_requestedorders are excluded from "unpaid order" detection.- Placing an order calls
POST /customer/api/orders(statuspending). The chef works per dish:PUT /chef/orders/{id}/items/{itemId}/accept|reject(reject takes a{"reason": ...}body).OrderItem.statusispending/accepted/rejected; the order status is derived (any accepted →accepted, else any pending →pending, else all rejected →rejected). The customer gets a live snackbar/dialog per decision; rejected items are excluded from bills and totals. "Delivered" (PUT /chef/orders/{id}/complete) is blocked while any item is still pending. - Get Bill (
PUT /customer/api/orders/{id}/payment) marks the orderpayment_requested(no payment gateway — settled at the counter). Admin sees the bill on/adminand Generate Bill forcompleted/payment_requested/paidorders; Mark as Paid (PUT /admin/orders/{id}/paid) setspaid, increments the customer'svisit_count, and frees that exact table+slot (green) — only when no other unpaid order is on the slot. Admin can also free any slot manually from/admin/tables.
- Run the backend (
python run.py, port 8001) andnpm start(port 3000). - The QR PNG now embeds your LAN IP automatically (backend
app/utils/network.py); the frontend also points API calls at that LAN host in dev (src/utils/apiBaseUrl.js). - Regenerate the QR after any URL-related change — the printed PNG keeps the old URL.
- If the phone can't connect, allow Node/Python through Windows Firewall for private networks.
backend/render.yamldefines thetabble-backendweb service (free tier, Singapore): start commanduvicorn app.main:app --host 0.0.0.0 --port $PORT --workers 1, persistent 1 GB disk forapp/static/images, auto-deploy from branchtabblefinal-backend.- Caveats:
FRONTEND_URLis a placeholder (https://your-frontend-url.com),CORS_ORIGINS="*"conflicts with the code's forcedallow_credentials=True, andADMIN_PASSWORDis hardcoded. Set real env vars in the Render dashboard before production.
- Frontend is plain JavaScript — there is no TypeScript. There is no CI and no standalone linter in the repo; backend tests run via pytest (see above), the frontend's only validation is
npm run build(and E2E is manual — the phone/LAN test below). - UI end-to-end (scan → order → chef → pay) is deliberately not automated: it needs a real browser + phone, so it's covered manually with the phone test below. The API-level equivalent is in the integration tests.
- The React SPA is also served by the backend when
frontend/buildexists (mounted at/inmain.py), which shadows the legacy Jinja2 HTML routes (/chef,/customer,/admin...). Thetemplates/dir those routes reference doesn't exist. app/services/optimized_queries.pyis dead, unimported code that references non-existent columns — do not wire it up as-is.__init__.pyis missing inbackend/app/,app/routers/,app/models/(namespace packages work, but tooling may complain).- Redux store (
src/store/) and React Query exist, but the customer menu uses theuseMenuOptimizedhooks instead; several services/hooks/components are unimported dead code (see AGENTS.md). frontend/.env,frontend/.env.production,frontend/build/, andfrontend/node_modules/do not follow ignore rules (no root.gitignore) — don'tgit add -Ablindly.
For agents working in this repo, see AGENTS.md for the compact, verified operator guide.
Tabble v3.1 — QR ordering. Proprietary; all rights reserved.