<div align="center">#🍽️ QR Menu###The QR code on the table isn't a gimmick here — it's the entire auth layer.**A production-shaped, multi-tenant restaurant ordering platform.**Guests order without an app or account.Kitchens see it the instant it lands, over a live socket, not a refresh button.Every write path a restaurant owner touches is gated behind a subscription state that's re-checked *on every request*, not just at login.<p><img alt="NestJS" src="https://img.shields.io/badge/Backend-NestJS%2011-E0234E?logo=nestjs&logoColor=white"><img alt="Next.js" src="https://img.shields.io/badge/Frontend-Next.js%2016%20(App%20Router)-000000?logo=nextdotjs&logoColor=white"><img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-5.7-3178C6?logo=typescript&logoColor=white"><img alt="MongoDB" src="https://img.shields.io/badge/MongoDB-Mongoose%209%20%2B%20Transactions-47A248?logo=mongodb&logoColor=white"><img alt="Socket.IO" src="https://img.shields.io/badge/Realtime-Socket.IO-010101?logo=socketdotio&logoColor=white"><img alt="Zustand" src="https://img.shields.io/badge/State-Zustand-2F2F2F?logo=react&logoColor=61DAFB"><img alt="Tailwind" src="https://img.shields.io/badge/UI-TailwindCSS%20v4-06B6D4?logo=tailwindcss&logoColor=white"><img alt="i18n" src="https://img.shields.io/badge/i18n-AR%20%7C%20EN%20%7C%20TR-8A2BE2"><img alt="Tests" src="https://img.shields.io/badge/Backend%20Tests-225%2B%20passing-C21325?logo=jest&logoColor=white"><img alt="License" src="https://img.shields.io/badge/License-UNLICENSED-lightgrey"></p></div>---##Whythis README exists, and why it leans hard on the backend
This is the **root of a monorepo**with two independently deployable applications —`backend/`(NestJS+MongoDB) and `frontend/`(Next.js AppRouter).Each has its own README.This document is not a third copy of either — it's the map that shows how they form one system, written for someone who needs to understand *the whole machine* well enough to run it, extend it, or interview the person who built it.It leans deliberately toward the backend, because that's where this system's actual hard problems live: an atomic multi-collection order transaction, a subscription gate re-evaluated on every single request, a real-time layer that has to re-implement auth outside Nest's guard pipeline because sockets don't go through HTTP guards, and a table-availability model that's a *count*, not a boolean. The frontend is a well-organized, feature-complete Next.js client consuming all of that — and it gets a full architectural treatment here too — but the interesting engineering decisions are concentrated on the API side, so that's where this document spends most of its words.If you only read one section, read [§6—TheOrderLifecycle,End to End](#6-the-order-lifecycle-end-to-end).It's the single request that touches every layer ofthis stack at once.---##TableofContents1.[WhatThisActuallyIs](#1-what-this-actually-is)2.[MonorepoMap](#2-monorepo-map)3.[SystemArchitecture](#3-system-architecture)4.[Backend,InDepth](#4-backend-in-depth)-[4.1DesignPrinciplesThatAren't ObviousFrom the CodeAlone](#41-design-principles-that-arent-obvious-from-the-code-alone)-[4.2DataModel](#42-data-model)-[4.3StateMachines](#43-state-machines)-[4.4TheReal-TimeLayer](#44-the-real-time-layer)-[4.5 API Surface](#45-api-surface)-[4.6TestingStrategy](#46-testing-strategy)-[4.7Trade-offs &KnownLimitations—AnHonestAccount](#47-trade-offs--known-limitations--an-honest-account)5.[Frontend,InDepth](#5-frontend-in-depth)6.[TheOrderLifecycle,End to End](#6-the-order-lifecycle-end-to-end)7.[TheFrontend↔BackendContract](#7-the-frontend--backend-contract)8.[GettingStarted](#8-getting-started)9.[ScriptsReference](#9-scripts-reference)10.[Roadmap](#10-roadmap)11.[License](#11-license)---##1.WhatThisActuallyIs**QR Menu** is the platform behind a restaurant's entire digital ordering loop, built as two cooperating apps:- A **guest never installs anything or signs up**.They scan the QR code glued to their table, land on a menu scoped to that exact table, add items to a cart, and place an order.Behind the scenes that QR scan mints a short-lived, table-scoped session — no password, no account, but not anonymous either: it's a real, authorizable identity for the next two hours.-The**restaurant owner** runs their entire operation — menu, tables, live order queue, staff — from a Next.js dashboard.The moment a guest's order lands, it appears on their screen over a live socket. As they move it through **Pending → Preparing → Ready → Completed**, the guest's own screen updates in the same channel,with no polling anywhere in the loop.-An**admin** sits above all of it, approving new restaurants, watching subscriptions lapse and renew, and holding the one lever (`SubscriptionGuard`) that turns a restaurant's write access on and off.Layered underneath all three of those journeys is a SaaS subscription gate that isn't just checked once at login — it's re-evaluated on *every single guarded request*, meaning a restaurant whose subscription lapses at 3:14 PM loses write access at 3:14 PM, mid-session, not the next time someone signs in.---##2.MonorepoMap```Qr menu/├── backend/NestJS API — the source of truth for everything
│├── src/││├── controllers/ thin — DTO validation in, service call out
││├── services/ all business logic +Mongoose queries
││├── schemas/Mongoose schema classes + indexes
││├── dtos/class-validator request shapes, one folder per resource
││├── guards/Auth→UserActive→Subscription→Roles││├── decorators/@Roles(...) metadata
││├── gateways/OrderGateway,NotificationGateway(Socket.IO)││├── adapters/AuthSocketAdapter— handshake-level JWT auth for sockets
││├── token/ access/refresh JWT signing & verification
││├── mailers/ password-reset OTP email delivery
││├── crons/ nightly subscription-expiry sweep
││├── libs/ response envelope, assertOwnerOrAdmin, cloudinary, messages
││├── enums/OrderStatus,SubscriptionStatus,TableStatus,UserRole,NotificationType││└── specs/services/225+ unit tests, mirroring src/services/│└── README.md ← the exhaustive backend write-up (sequence traces, full endpoint table)│├── frontend/Next.js 16AppRouter client
│├── app/││├──(auth)/ login,register, forgot-password — unauthenticated route group
││├──(dashboards)/ dashboard/(owner) and admin/— authenticated route groups
││├──(pages)/ menu/[qrToken], restaurant/[id], profile, notifications, marketing pages
││├── apis/ one fetch-wrapper module per backend resource
││├── hooks/ one data hook per resource,+ hooks/customs (useLanguage, useFormErrors)││├── stores/ useAuthStore, useNotificationStore (Zustand)││├── socket/ singleton Socket.IO client, lifecycle tied to auth state
││├── providers/LanguageProvider(i18n context)││├── locales/ ar.ts / en.ts / tr.ts — full trilingual UI copy
││├── routes/ shared guard/fallback screens (e.g.AdminOnly)││└── components/ landing sections, navbar, footer, shared UI primitives
│└── README.md (stock create-next-app doc —this root README supersedes it)│└──.git/```**Ruleof thumb for navigating the code:**if it's a decision about *what's allowed to happen*, it lives in `backend/src/guards` or `backend/src/services`.If it's a decision about *what it looks like or how it's phrased*, it lives in `frontend/app`.The frontend does not re-implement authorization logic anywhere — it reacts to `401`/`403` responses and socket events, nothing more.---##3.SystemArchitecture```mermaid
graph TB
subgraph ClientsGuest["📱 Guest Device\nno account — QR scan only"]Owner["💻 Restaurant Dashboard\n(dashboards)/dashboard"]AdminUI["🛡️ Admin Console\n(dashboards)/admin"]
end
subgraph FE["Next.js 16 — App Router"]
direction TB
RouteGroups["(auth) · (dashboards) · (pages)"]ApiLayer["app/apis — per-resource fetch wrappers\ncredentials: 'include' on every call"]Hooks["app/hooks — data + mutation hooks"]Stores["Zustand: useAuthStore / useNotificationStore"]SocketClient["app/socket — singleton, connects only when authenticated"]
I18n["LanguageProvider + ar/en/tr locales"]
end
subgraph BE["NestJS API"]
direction TB
CORS["CORS(credentials:true) + cookie-parser"]VPipe["Global ValidationPipe\n(whitelist + single-error-message factory)"]Guards["Guard Pipeline\nAuth → UserActive → Subscription → Roles"]Interceptor["ResponseInterceptor → { success, message, data }"]Controllers["Auth · Restaurant · Category/Product · Table\nOrder/OrderItem · Subscription · GuestSession · User"]RealTime["AuthSocketAdapter → OrderGateway + NotificationGateway"]Cron["SubscriptionCronService · nightly expiry sweep"]
end
subgraph Data["Persistence"]Mongo[("MongoDB\nMongoose · multi-document transactions")]
end
subgraph ExternalCloudinary["☁️ Cloudinary — logo/cover/product images"]
SMTP["✉️ SMTP — password-reset OTP codes"]
end
Guest-->|HTTPS + WS|RouteGroupsOwner-->|HTTPS + WS|RouteGroupsAdminUI-->|HTTPS|RouteGroupsRouteGroups-->ApiLayer--> CORS
RouteGroups-->SocketClientApiLayer<-->HooksHooks<-->Stores
CORS -->VPipe-->Guards-->Controllers-->InterceptorControllers<-->MongoControllers-->CloudinaryControllers--> SMTP
Controllers-.triggers.->RealTimeSocketClient<-.socket.io.->RealTimeCron-->MongoCron-.notifies.->RealTime```**Readthis diagram as two arrows, not one system.**Every write from either dashboard or the guest device goes `RouteGroups→ApiLayer→ CORS →Guards→Controllers→Mongo`.Every live update flows the opposite direction, outside that entire HTTP pipeline, through `RealTime→SocketClient`.The frontend never polls for order status — it either has an active socket subscription or it doesn't, and the fallback for"socket not connected yet" is a normal authenticated `GET`, not a timer.---##4.Backend,InDepth###4.1DesignPrinciplesThatAren't ObviousFrom the CodeAlone**Ownership is enforced in one function, not re-derived per query.**Rather than scoping every Mongoose query by `restaurantId`(trivially easy to forget on one query and leak cross-tenant data), sensitive mutations fetch the resource first, then call a single shared assertion:```ts
assertOwnerOrAdmin({
ownerId: restaurant.userId.toString(),
authUser,
message: messages.order.forbidden,});```It costs one extra document read per mutation.In exchange,"can this user touch this row?" exists in exactly one function, unit-testable in isolation, instead of being silently re-implemented (and eventually drifting) across a dozen controllers.**The guard pipeline runs cheapest-check-first, on purpose:**```AuthGuardUserActiveGuardSubscriptionGuardRolesGuard(verify JWT,(1 DB read:(1 DB read:(0 DB reads —
no DB hit) is user blocked?) is subscription live?) decorator reflection)```
A forged or expired JWT dies before a single database round trip. A blocked user is rejected before the heavier subscription lookup runs.Roles are checked last because they're free — and only matter once the requester is already a legitimate, active, subscribed actor.**Subscription state is re-checked on every request, not cached at login.**`SubscriptionGuard` hits the `Restaurant` collection on every guarded request except for admins, who bypass it entirely. A restaurant whose subscription lapses at 3:14 PM loses write access at 3:14 PM — mid-session — because a billing gate that only matters at sign-in isn't really a gate.**One response envelope, enforced globally, never for errors.**Every controller returns `{ data, message }`; a global `ResponseInterceptor` wraps that into `{ success:true, message, data }`for every success response in the API.Errors are deliberately *not* reshaped by the interceptor — they're re-thrown through Nest's standard exception filters, so HTTP status codes stay meaningful and the frontend can branch on status code rather than parsing a message string.**Guests are first-class actors without being user accounts.** A `GuestSession` isn't a stripped-down `User` — it's its own schema with its own 2-hour TTL, tied to exactly one table.It's the identity an order is placed under, the room a socket subscribes to for status updates, and the thing invalidated when the table's tab closes.This keeps the guest experience genuinely account-free while still giving the backend something concrete to authorize against.**Placing an order is one atomic unit across four collections, not four separate writes.**`Order`,`OrderItem`,`Table`, and `Notification` are all touched inside a single Mongo session (`connection.startSession()`+`withTransaction`).If a product goes unavailable mid-write, the entire order rolls back rather than leaving a half-priced, half-populated order sitting in the database.The full trace is in [§6](#6-the-order-lifecycle-end-to-end).**Price is never trusted from the client.**`unitPrice` is resolved server-side from the product's *current* `discountPrice ?? price` at the moment each item is added to the order — a stale cached price or a tampered request body can't leak through.###4.2DataModel```mermaid
erDiagram
USER ||--o{ RESTAURANT : owns
RESTAURANT ||--|| SUBSCRIPTION : has
RESTAURANT ||--o{ CATEGORY : defines
RESTAURANT ||--o{ TABLE : has
CATEGORY ||--o{ PRODUCT : contains
RESTAURANT ||--o{ GUEST_SESSION : hosts
TABLE ||--o{ GUEST_SESSION :"scoped to"
GUEST_SESSION ||--o{ ORDER : places
TABLE ||--o{ ORDER :"occupies"
ORDER ||--|{ ORDER_ITEM : contains
PRODUCT ||--o{ ORDER_ITEM :"priced from"
USER ||--o{ NOTIFICATION : receives
GUEST_SESSION ||--o{ NOTIFICATION : receives
USER {ObjectId restaurantId
string email
string passwordHash
enum role "OWNER | MANAGER | ADMIN"
boolean isActive
}
RESTAURANT {ObjectId userId
string name
object logo
object cover
enum subscriptionStatus
}
SUBSCRIPTION {ObjectId restaurantId
enum status "PENDING | ACTIVE | EXPIRED"
date expiresAt
}
CATEGORY {ObjectId restaurantId
string name
number sortOrder
}
PRODUCT {ObjectId categoryId
ObjectId restaurantId
number price
number discountPrice
boolean isAvailable
}
TABLE {ObjectId restaurantId
string qrToken "unique, unguessable"enum status "AVAILABLE | OCCUPIED"}
GUEST_SESSION {ObjectId restaurantId
ObjectId tableId
string sessionToken
boolean isActive
date expiresAt "2h TTL"}
ORDER {ObjectId restaurantId
ObjectId tableId
ObjectId guestSessionId
enum status "PENDING|PREPARING|READY|COMPLETED"
number totalPrice
boolean isPaid
}
ORDER_ITEM {ObjectId orderId
ObjectId productId
number quantity
number unitPrice
}
NOTIFICATION {ObjectId userId
ObjectId guestSessionId
enum type
boolean isRead
}```The catalog today is a real two-level hierarchy —**Category→Product**,with server-resolved pricing/discounts and an `isAvailable` flag the order-creation path actually checks. A further **Product→Variant→Addon** layer (size options, extras) is on the roadmap but isn't in the current schema — worth knowing before you go looking for it.###4.3StateMachines**Order**— a strict one-way graph, enforced with an explicit guard clause before every transition (`order.status !== PENDING` throws before it ever reaches `PREPARING`):```mermaid
stateDiagram-v2
[*]--> PENDING: guest places order
PENDING --> PREPARING: owner accepts
PREPARING --> READY: kitchen marks ready
READY --> COMPLETED: owner completes + isPaid=true
COMPLETED -->[*]: close-session
```**Table**— availability is a *count*of non-terminal orders, not a flag flipped on a single order's completion. A table of six ordering in two separate rounds stays `OCCUPIED` until *zero* active orders remain against it:```mermaid
stateDiagram-v2
AVAILABLE --> OCCUPIED: first order placed
OCCUPIED --> OCCUPIED: another order placed at same table
OCCUPIED --> AVAILABLE: last active order reaches COMPLETED
```**Subscription**— driven both by admin action and a nightly cron sweep:```mermaid
stateDiagram-v2
[*]--> PENDING: restaurant registers
PENDING --> ACTIVE: admin approves
ACTIVE --> EXPIRED: cron sweep finds expiresAt passed
EXPIRED --> ACTIVE: admin re-approves
```###4.4TheReal-TimeLayerThe subtlety worth calling out explicitly:**WebSocket connections never pass through Nest's HTTP guard pipeline.** `AuthGuard`, `UserActiveGuard`, etc. are HTTP-request constructs — a socket handshake is a different transport entirely. This is solved with a custom `IoAdapter` (`AuthSocketAdapter`) that intercepts every incoming socket connection *before* it's accepted: it reads the same `token` cookie the HTTP layer uses (falling back to an `Authorization:Bearer` header for non-browser clients), verifies it with the exact same `TokenService.verifyAccessToken`, and rejects the handshake outright on failure — the socket never even opens.One auth implementation, trusted by two transports, instead of duplicated logic that drifts apart over time.|Gateway|Audience|Presence registry |Emits||---|---|---|---||`OrderGateway`|Restaurant dashboards *and* guest devices |In-memory, keyed by `userId`/`guestSessionId`|`new-order`→ staff ·`order-status-updated`→ the specific guest session ||`NotificationGateway`|Authenticated users only |In-memory, supports multiple concurrent sockets per user (multi-tab/device)|`receive-notification`|###4.5 API Surface|Domain|Key endpoints |Guards|Notes||---|---|---|---||Auth|`register`·`login`·`logout`·`me`·`forgot/verify/reset-password`·`refresh`| mostly public/ self |5 failed logins →24h lockout · refresh tokens hashed at rest · OTP emailed & bcrypt-hashed,10-min expiry ||GuestSessions|`POST /guest-sessions/:qrToken`|public|The entire guest onboarding step — idempotent per table within its 2h window ||Restaurants| full CRUD |Auth,UserActive,Roles|Multipart logo + cover upload via Cloudinary||Menu(Category/Product)| full CRUD |Auth,UserActive,**Subscription**,Roles| A lapsed subscription blocks menu edits, not just new orders ||Tables| full CRUD |Auth,UserActive,Subscription,Roles|`qrToken` is the unguessable key the guest flow hinges on ||GuestMenu|`GET /guest-menu/:sessionToken`| session-token scoped |The guest-facing read path — no JWT required ||Orders|`POST /orders`(session-token)·`accept/ready/complete/close-session`(Roles:Manager)·`me`·`admin/:restaurantId`| mixed — see table |Each transition enforces its own precondition on current status ||Subscriptions|`active/expired/pending`·`POST /subscriptions/:restaurantId`|Roles:Admin|Approves a pending or expired restaurant ||Notifications| full CRUD |Auth,UserActive|Scoped to the requesting user ||Users| list/get/delete(block)· update profile |Auth,UserActive,Roles|Delete= block, not a hard delete|Full request/response shapes and validation rules are generated automatically at **`GET /api/docs`**(Swagger).This table is the map;Swagger— and the exhaustive endpoint-by-endpoint breakdown in `backend/README.md`— is the territory.###4.6TestingStrategyThe suite is **unit-first and mock-heavy by design**: every service test mocks its Mongoose models and collaborating services, so business logic (state transitions, ownership checks, price resolution) is asserted without a live MongoDB in CI.```
backend/src/specs/services/14 spec files ·225+ test cases
├── order.service.spec.ts ← the transaction-heavy path, most heavily tested
├── order-item.service.spec.ts ← price-resolution logic in isolation
├── subscription.service.spec.ts ← state-machine transitions + cron sweep
├── guest-session.service.spec.ts ← idempotent-session logic
└──...one file per service
```Controller specs mirror this one level up, asserting each controller is a *thin* delegator — correct arguments passed through, correct value returned, nothing more — because controllers here carry no business logic by design.What's deliberately **not** covered by mocks (and would need a real integration/e2e pass to be trustworthy): actual Mongo transaction rollback behavior, real index usage under load, and the socket handshake rejecting a bad cookie against a live server.The**frontend currently ships without an automated test suite**— its correctness net today is TypeScript's type checking plus manual QA across the three route groups. That's the honest state of things, not an oversight to gloss over.###4.7Trade-offs &KnownLimitations—AnHonestAccount-**In-memory socket presence registries don't survive a restart and don't scale past one Node process.**Both gateways keep "who's connected" in plain JS memory.Behind a load balancer with more than one instance, a guest connected to instance A never receives an event emitted from instance B.This is the actual driver behind the Redis work in [§10](#10-roadmap)— not a nice-to-have, but the fix for a real horizontal-scaling ceiling.-**The transaction wraps a socket emit.**`mongoSession.withTransaction()` retries its callback on certain transient Mongo errors.If a retry happens *after*`sendNewOrder(...)` already fired once, the kitchen could theoretically see a duplicate `new-order` event for one successful order.Rare under normal load, but a known sharp edge.-**Cookie flags (`secure`,`sameSite`) are hardcoded for local development** rather than derived from `NODE_ENV`— called out explicitly in the source as a pending environment-based switch before a production deploy.-**`Variant`/`Addon` are not in the current schema**, despite being part of the original product vision — the catalog today is a real Category→Product hierarchy, not a flat list, but it stops one level shortof size/extras options.-**`assertOwnerOrAdmin` costs an extra document read per protected mutation**— a conscious trade for a single, auditable source of truth over the marginally cheaper alternative of scoping every query by `restaurantId` up front.---##5.Frontend,InDepthThe frontend is a **Next.js 16AppRouter** client organized around three route groups that map directly onto the three actors above:-**`app/(auth)/`**—`login`,`register`,`forgot-password`.No layout chrome, purely functional.-**`app/(dashboards)/`**—`dashboard/`(restaurant owner: home, categories, products, tables, orders, restaurant settings, subscription status, support) and `admin/`(approve/reject restaurants, subscription lists, user blocking).-**`app/(pages)/`**— everything else: the marketing site (`about`,`features`,`faqs`,`contact`), account pages (`profile`,`notifications`,`user/[id]`), restaurant management (`create-restaurant`,`edit-restaurant/[id]`,`restaurant/[id]`), and — the one page a guest ever sees —**`menu/[qrToken]`**.**Data access is a two-layer split, mirroring the backend's controller/service split:** `app/apis/*.ts` holds one thin fetch-wrapper module per backend resource (`auth.ts`, `order.ts`, `table.ts`, `guest-menu.ts`, …), all funneled through a single `request()` helper that sets `credentials: 'include'` on every call and transparently retries once through `/api/auth/refresh` on a `401`.`app/hooks/*.ts` sits one layer up, giving components resource-shaped hooks (`useOrder`, `useTable`, `useGuestSession`, …) instead of raw fetch calls.
**State that needs to survive across routes lives in two Zustand stores** — `useAuthStore` (the current user, read by the socket client to decide whether to connect at all) and `useNotificationStore` (the live notification feed, written to both by REST fetches and by the `receive-notification` socket event landing in the same store).
**The socket client is a lazily-created singleton** (`app/socket/socket.ts`) that refuses to connect if `useAuthStore` has no user, and is explicitly torn down on logout — so an unauthenticated tab never even attempts the handshake the backend's `AuthSocketAdapter` would reject anyway.
**i18n is a first-class concern, not an afterthought:** `app/locales/{ar,en,tr}.ts` hold complete UI copy trees, served through `LanguageProvider` and consumed via the `useLanguage()` hook everywhere from dashboard tables to the guest menu. Arabic, English, and Turkish are treated as equally primary, not a base language with two partial translations bolted on.
**On the guest side, `menu/[qrToken]/` is its own small, self-contained app**: `Header`, `Categories`, `Search`, `Products`, a `ProductDetailsModal`, a `CartDrawer`, and explicit states for `Loading`, `Empty`, `TableNotAvailable`, `ConfirmedOrder`, and `SuccessOrder` — the full set of edge cases a real guest hits between scanning a code and watching their order move to `READY`.
**UI toolkit:** Tailwind CSS v4 for styling, Framer Motion for the marketing-page and cart-drawer motion, `lucide-react` / `react-icons` for iconography, Swiper for carousels, `react-hot-toast` for the toast layer the backend's structured error messages feed straight into, and both `qrcode` and `react-qr-code` for generating the actual per-table codes the whole system hinges on.
---
## 6. The Order Lifecycle, End to End
This is the one request worth understanding completely — it's the only path in the system that touches the guest device, both frontend layers, the full backend guard/transaction stack, and both socket gateways in a single user action.
```mermaid
sequenceDiagram
autonumber
actor Guest
participant FE as Next.js (menu/[qrToken])
participant API as NestJS API
participant DB as MongoDB (transaction)
participant GW as OrderGateway
participant Owner as Dashboard (Next.js)
Guest->>FE: Scans table QR code
FE->>API: POST /api/guest-sessions/:qrToken
API->>DB: find Table by qrToken (must be active)
API->>DB: find Restaurant, check isActive + subscriptionStatus
alt no active GuestSession for this table
API->>DB: create GuestSession (2h TTL)
else session already exists
API->>API: reuse existing sessionToken (idempotent scan)
end
API-->>FE: { sessionToken }
FE->>FE: store token, render menu (Categories/Products)
Guest->>FE: builds cart in CartDrawer, taps "Place order"
FE->>API: POST /api/orders (header: x-session-token)
API->>DB: begin transaction
API->>DB: validate GuestSession is active & not expired
API->>DB: create Order (status=PENDING, totalPrice=0)
API->>DB: flip Table AVAILABLE → OCCUPIED
loop for each cart line
API->>DB: fetch Product, verify isAvailable
API->>API: unitPrice = product.discountPrice ?? product.price
API->>DB: create OrderItem, increment Order.totalPrice
end
API->>DB: create Notification (NEW_ORDER, target=owner)
API->>DB: commit transaction
API->>GW: sendNewOrder(restaurantId, order)
GW-->>Owner: emit "new-order"
API-->>FE: { success, data: order }
FE-->>Guest: ConfirmedOrder screen
Note over Owner: Owner taps "Accept" in the live queue
Owner->>API: PATCH /api/orders/:id/accept
API->>DB: guard — status must be PENDING → PREPARING
API->>GW: sendOrderStatus(guestSessionId, PREPARING)
GW-->>FE: emit "order-status-updated"
FE-->>Guest: tracker updates, no refresh
Note over Owner: repeats through Ready, then Completed
Owner->>API: PATCH /api/orders/:id/complete
API->>DB: status → COMPLETED, isPaid=true
API->>DB: count remaining active orders for this table
alt zero active orders remain
API->>DB: flip Table back to AVAILABLE
end
Owner->>API: PATCH /api/orders/:id/close-session
API->>DB: GuestSession.isActive = false
```
Two details worth internalizing from this trace: **the guest never sends a price**, and **the table's status is never a single order's problem to flip** — it's a live count evaluated fresh every time an order reaches a terminal state.
---
## 7. The Frontend ↔ Backend Contract
The backend is built expecting exactly this frontend, and the contract has three distinct shapes depending on who's calling:
| Actor | Identity carrier | How the frontend sends it |
|---|---|---|
| Restaurant owner / Admin | JWT access + refresh, `httpOnly` cookies | `credentials: 'include'` on every `fetch` — never touched by JS, never stored in `localStorage` |
| Guest | `sessionToken` from `POST /guest-sessions/:qrToken` | Sent as an `x-session-token` header, held in memory/`sessionStorage` on the guest device only |
| Socket clients (both) | Same `token` cookie, re-verified at handshake by `AuthSocketAdapter` | `io(url, { withCredentials: true })` |
Every successful REST response — regardless of actor — arrives as `{ success: true, message, data }`, which is exactly what `app/apis/request.ts` unwraps before handing `data` to a hook. A `401` anywhere except `/api/auth/refresh` itself triggers exactly one silent refresh attempt before the frontend gives up and routes to `(auth)/login`.
---
## 8. Getting Started
**Prerequisites:** Node.js 20+, a MongoDB connection string (Atlas or local), a Cloudinary account, an SMTP-capable mail account.
Run both apps from two terminals — this is two separate `npm install`s, two separate dev servers, on two separate ports.
```bash
# Terminal 1 — backend (default port 9090)
cd backend
npm install
cp .env.example .env # fill in the values in the table below
npm run start:dev
# Terminal 2 — frontend (default port 3000)
cd frontend
npm install
npm run dev
```
**Backend environment (`backend/.env`):**
| Variable | Used by |
|---|---|
| `PORT` | `main.ts` — server listen port (defaults to `9090`) |
| `MONGO_URI` | Mongoose connection |
| `JWT_SECRET` / `JWT_REFRESH_SECRET` | Access (15m) / refresh (30d) token signing |
| `CLOUDINARY_CLOUD_NAME` / `CLOUDINARY_API_KEY` / `CLOUDINARY_API_SECRET` | Restaurant & product image uploads |
| `MAIL_HOST` / `MAIL_PORT` / `MAIL_USER` / `MAIL_PASS` / `MAIL_FROM` | Password-reset OTP emails |
**Frontend:** points at the backend via `BACKEND_URL` in `app/apis/request.ts` (defaults to `http://localhost:9090`) — update it there or promote it to a `NEXT_PUBLIC_API_URL` env var before deploying anywhere the two apps aren't both on `localhost`.
Once the backend is running:
- **Swagger docs:** `GET /api/docs`
- **Health check:** `GET /api/health`
- **Human-readable status page:** `GET /`
Once both are running, open `http://localhost:3000`, register an owner account, create a restaurant, generate a table QR from the dashboard, then open `http://localhost:3000/menu/:qrToken` in a second tab or on a phone to play the guest side live against your own dashboard.
---
## 9. Scripts Reference
| Location | Command | Does |
|---|---|---|
| `backend/` | `npm run start:dev` | Watch-mode dev server |
| `backend/` | `npm run build` / `start:prod` | Compile to `dist/` / run the compiled build |
| `backend/` | `npm run seed` | Seed the database via `src/seed.ts` |
| `backend/` | `npm run test` / `test:watch` / `test:cov` | Unit tests |
| `backend/` | `npm run test:e2e` | Supertest-driven HTTP-level tests |
| `backend/` | `npm run lint` / `format` | ESLint (autofix) / Prettier |
| `frontend/` | `npm run dev` | Next.js dev server |
| `frontend/` | `npm run build` / `start` | Production build / serve it |
| `frontend/` | `npm run lint` | ESLint |
---
## 10. Roadmap
Redis is the next infrastructure addition, and it lands in three specific places for three specific reasons — not "add Redis and cache everything":
1. **Read-through cache for the menu catalog** — category/product reads are the highest-volume, lowest-change-frequency queries in the system. `GET`-then-`SETEX`, keyed by `restaurantId`, invalidated explicitly on the corresponding `update`/`destroy` calls rather than a blind TTL, so a price change is never served stale.
2. **A Redis-backed Socket.IO adapter (`@socket.io/redis-adapter`)**, replacing the in-memory presence registries described in [§4.7](#47-trade-offs--known-limitations--an-honest-account) — the actual fix for running more than one Node process behind a load balancer without a guest missing a status update.
3. **A shared store for guest-session lookups** — `getActiveSession(sessionToken)` currently round-trips Mongo on every order-related guest request. Given a 2-hour TTL, it's an ideal Redis key with a matching expiry, removing a Mongo hit from the hottest guest-facing path in the system.
Beyond Redis, roughly in priority order: **`Product → Variant → Addon`** to complete the originally-scoped catalog depth, a real payment gateway integration (`isPaid` is currently a manual flag flipped by staff), rate limiting on the auth and guest-session endpoints, structured audit logging on admin actions, environment-derived cookie flags for production, and an automated frontend test suite to match the backend's.
---
## 11. License
UNLICENSED — private/proprietary. All rights reserved unless a license file states otherwise.
<div align="center">
*Built for a service industry that runs on tickets, tables, and timing — the stack just makes sure none of it gets lost between the QR code and the kitchen.*
</div>
السؤال
Zen Eddin Allaham
لدي مشروع قمت ب بنائه ب NestJS Next.js وهو مشروع يعمل عبر Production
لكن اريد منكم مراجعة README.md واخباري هل يوجد اي نقاط ضعف في مشروع او ثغرات او اي شي يجب تحسينه
وللعلم انا لم استخدم Redis لان لسا مشروع جديد ولم يظهر اي ضغط حتى الان
رابط مستودع : https://github.com/ZenZN99/DineFlow
2 أجوبة على هذا السؤال
Recommended Posts
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.