This document summarizes findings from open-source reservation systems and proposes a concrete plan for adding table reservation to pos2.
Two user groups are essential (in scope for the first release):
- Staff – Create, edit, cancel, and list reservations; seat reservations at tables; finish reservations; see table status (available / reserved / occupied) on the table canvas.
- Web end users (customers) – Reserve a table via the web (date, time, party size, name, phone); view their reservation(s); cancel their reservation. No login required for the minimal flow (identify by phone or booking reference).
Optional for a later phase: logged-in “My reservations,” email/SMS confirmations, opening-hours enforcement.
| Repo | Stack | Reservation model | Table ↔ reservation | Best for |
|---|---|---|---|---|
| skurshner/restaurant-reservations (Seatable) | React, Express, Knex, PostgreSQL | date, time, people, first/last name, mobile, status (booked → seated → finished) | Table has reservation_id; “seat” sets it, “finish” clears it |
API shape, status flow |
| slavyanHristov/restaurant-table-reservation-system (RTRS) | Vue, Express, Sequelize, MySQL | resDate, resTime, people, resStatus (pending|seated|missed), customer FK | Table belongs to reservation; seat = assign table; free = clear | Seat/free flow, validations |
| ewradcliffe/restaurant-booking-system | Django, PostgreSQL | name, date, time, guests, email, booked_by (user) | No table assignment (out of scope) | CRUD, auth, past-date validation |
| jossansik/Portfolio-project4-restaurant-booking | Django, Heroku Postgres | Table, Reservation, Menu; date/time/guests; staff confirm | Tables + capacity; one active reservation per user; availability by date + guests | Availability, capacity, one-booking-per-user |
| pjborowiecki/SEAT-FRENZY | Next.js 14, Drizzle, PlanetScale | Full booking + venue listing | Multi-tenant, booking management | Product vision (not schema detail) |
| natsumi-h/chopeseats | MERN, Mantine | User, Booking, Restaurant | High-level only in README | — |
- booked – Reservation created (date, time, party size, contact). No table required yet.
- seated – Staff assigns a table; reservation is “seated” at that table (table holds
reservation_idor reservation holdstable_id). - finished – Guest done; table cleared, reservation closed.
- cancelled – Reservation cancelled (no table link).
- available – No active order, no reservation assigned (or reservation finished/cancelled).
- reserved – Assigned to a reservation that is still booked (future or current slot); no order yet.
- occupied – Has active order (current pos2 “occupied”) or reservation seated here and order may exist.
So “reserved” = table is assigned to an upcoming/current reservation but guests not yet seated (no order). After seating, we already have “occupied” via existing order flow.
- Reservation (new): tenant-scoped; date, time, party_size, customer name/phone (and optionally user_id); status (booked | seated | finished | cancelled); table_id (optional, set when staff “seats” the reservation).
- Table (existing): add optional reservation_id (or rely only on Reservation.table_id). Either:
- Option A – Table has
reservation_id: when we seat, settable.reservation_idand reservation.status = seated; when we finish, cleartable.reservation_idand set reservation.status = finished. “Reserved” then means: some reservation with status=booked has this table_id. - Option B – Only Reservation has
table_id. Table status “reserved” = exists reservation with table_id = this table and status = booked. No FK on Table. Simpler schema change; table status is derived from reservations.
- Option A – Table has
Recommendation: Option B (only Reservation.table_id). Table has no new column; “reserved” is computed: “exists reservation where table_id = table.id and status = 'booked' and date/time in relevant window”.
- Reservations: create, list (by date, by phone, by status), get by id, update, update status (including cancel).
- Tables: list (with status: available | reserved | occupied), “seat” (assign reservation to table → set reservation.table_id and status=seated; create or attach order if needed), “finish” (clear table assignment, set reservation to finished).
pos2 already has table activation (PIN, active order). Reservation “seating” can create or attach to the same order flow (activate table + set reservation.table_id and status=seated).
- Table: id, name, token, floor_id, layout, seat_count, order_pin, is_active, active_order_id, activated_at. No reservation link.
- Order: table_id, status, items, payment, etc. No reservation_id.
- GET /tables/with-status: returns
availableoroccupiedfrom presence of active order (pending/preparing/ready). No “reserved”. - Frontend:
CanvasTable.statusis'available' | 'occupied' | 'reserved'and i18n has TABLES.RESERVED / TABLE_STATUS.reserved; UI only branches on occupied vs non-occupied (no reserved handling yet).
-
Migration
- Add table
reservation(tenant_id, customer_name, customer_phone, reservation_date, reservation_time, party_size, status, table_id nullable FK to table, token unique nullable for public lookup/cancel, created_at, updated_at). - status: enum or string one of:
booked,seated,finished,cancelled. - token: unique string (e.g. UUID), generated on create; used by end users to view/cancel without login.
- Indexes: tenant_id, (tenant_id, reservation_date), (tenant_id, status), table_id, token (unique).
- Add table
-
Models (SQLAlchemy/ORM)
Reservationwith fields above; relationship toTable(optional).- Pydantic schemas: ReservationCreate, ReservationUpdate, ReservationStatusUpdate, ReservationOut.
-
Endpoints
POST /reservations– create (status=booked); validate date/time not in past. Used by staff (authenticated) and end users (public endpoint, see Phase 4).GET /reservations– list by tenant; query params: date, status, phone (search). Staff only (permission RESERVATION_READ).GET /reservations/{id}– get one. Staff by id; end users by id + token or phone (see Phase 4).PUT /reservations/{id}– update (date, time, party_size, name, phone); only if status=booked. Staff only (or end user for own reservation with token).PUT /reservations/{id}/status– set status (cancel → cancelled; seat → seated and set table_id). Cancel: staff or end user (with token); seat/finish: staff only.- Permissions: RESERVATION_READ, RESERVATION_WRITE for staff; public
POST /reservations(and optionally public get/cancel by token) for end-user booking.
-
Table status
- In
GET /tables/with-status, compute three states:- occupied: table has active order (existing logic) OR has a reservation with status=seated and table_id=this table.
- reserved: no active order and exists reservation with table_id=this table and status=booked and reservation_date >= today (and optionally time window).
- available: otherwise.
- Return
statusas"available" | "reserved" | "occupied".
- In
-
Seat / finish
PUT /reservations/{id}/seat– body:{ "table_id": number }. Checks: reservation status=booked; table free (not occupied, not reserved by another); seat_count >= party_size. Set reservation.table_id and reservation.status=seated. Optionally auto-activate table and create order (reuse existing activate flow) so table becomes “occupied”.PUT /reservations/{id}/finish(orPOST /reservations/{id}/finish) – set reservation.status=finished, reservation.table_id=null. If you linked an order to the reservation, you can leave order/table as-is (staff closes table separately) or close table in one go; design choice.
-
Tables canvas
- Use existing
CanvasTable.statusand i18n. Add styling forstatus === 'reserved'(e.g. distinct color/pattern) and show “Reserved” in the side panel when selected table is reserved. - Optionally show reservation summary (date, time, party size, name) in panel.
- Use existing
-
Reservations list (staff)
- New view: list reservations by date (default today); filters status, search by phone/name. Actions: Edit, Cancel, Seat (open table picker), Finish.
- “Seat” opens modal with list of available tables (capacity >= party_size); on confirm call
PUT /reservations/{id}/seatwith chosen table_id.
-
Create / edit reservation (staff)
- Form: date, time, party size, customer name, phone. Submit → POST or PUT. Validate not in past.
-
Public API for customers
- Public
POST /reservations– no auth; body: tenant_id or tenant slug, reservation_date, reservation_time, party_size, customer_name, customer_phone (and optionally customer_email). Returns created reservation with token (e.g. UUID) for later lookup/cancel. Validate: date/time not in past; rate limiting per IP/phone. - Public
GET /reservations/by-token?token=xxx– return one reservation (masked or full) for “view my reservation” and cancel flow. Token is returned on create and optionally in confirmation copy. - Public
PUT /reservations/{id}/cancel(orPUT /reservations/{id}/statuswith body{ "status": "cancelled" }) – allowed only with valid token (e.g. query param or header) or for the reservation that matches token. Idempotent if already cancelled.
- Public
-
Customer booking page (frontend)
- Booking form: tenant context (e.g. from URL or tenant selector). Fields: date, time, party size, name, phone (required); optional email. Submit → call public
POST /reservations→ show success + reservation details + “View / Cancel” link (with token in URL or stored in sessionStorage). - View / Cancel reservation: page or modal that accepts token (from link). GET by token → show date, time, party size, status. Button “Cancel reservation” → confirm → call cancel API → show confirmation.
- No login required for minimal flow; token is the proof for view/cancel.
- Routing and tenant
- End-user booking must be reachable in a tenant-aware way (e.g.
/bookwith tenant in path or subdomain, or tenant slug). Reuse existing tenant resolution (e.g. from menu/public pages) so the booking form knows which tenant_id to send.
-
Validation
- Opening hours / closed days (optional for v1): reject or warn for out-of-hours or closed days.
- Max party size per table (seat_count) when seating.
- No double-booking: when assigning table_id, ensure no other booked reservation has same table_id in overlapping slot; simple version: one “reserved” per table at a time.
-
Notifications
- Optional (later): email/SMS confirmations or reminders.
New table: reservation
| Column | Type | Notes |
|---|---|---|
| id | serial PK | |
| tenant_id | int FK | tenant.id |
| customer_name | varchar | |
| customer_phone | varchar | for search |
| reservation_date | date | |
| reservation_time | time | |
| party_size | int | |
| status | varchar | booked, seated, finished, cancelled |
| table_id | int FK? | nullable, table.id; set when seated |
| token | varchar | unique; for end-user view/cancel without login |
| created_at | timestamptz | |
| updated_at | timestamptz |
Existing table
No schema change if we derive “reserved” from reservations where table_id = table.id and status = 'booked'.
Existing order
Optional: add reservation_id nullable FK later to link order to reservation when seated (helps analytics and “finish reservation” flow). Not required for MVP.
- skurshner/restaurant-reservations: reservation status (booked → seated → finished), table.reservation_id for seating, PUT/DELETE on
/tables/:id/seat, list by date and mobile_number. - slavyanHristov/restaurant-table-reservation-system: reservation (date, time, people, status), table capacity and “seat/free” flow.
- jossansik/Portfolio-project4-restaurant-booking: one active reservation per user, availability by date + guests, table capacity checks, staff confirm.
- ewradcliffe/restaurant-booking-system: CRUD, auth, past-date validation, no table assignment.
| Step | Task | Delivered |
|---|---|---|
| 1 | Migration: reservation table with tenant_id, customer_name, customer_phone, reservation_date, reservation_time, party_size, status, table_id (nullable), token (unique), created_at, updated_at |
SQL migration |
| 2 | ORM model Reservation + Pydantic schemas (Create, Update, StatusUpdate, Out, PublicCreate, PublicOut) |
back/app/models.py + schemas |
| 3 | Staff endpoints: POST/GET/PUT /reservations, GET /reservations/{id}, PUT /reservations/{id}/status (guarded by RESERVATION_READ / RESERVATION_WRITE) | back/app/main.py |
| 4 | GET /tables/with-status: add “reserved” (reservation with table_id and status=booked) and keep “occupied” (active order or seated reservation) | back/app/main.py |
| 5 | PUT /reservations/{id}/seat (body: table_id), PUT or POST /reservations/{id}/finish; validate capacity and availability | back/app/main.py |
| 6 | Public endpoints: POST /reservations (no auth; tenant from body/path; return token), GET /reservations/by-token?token=, PUT /reservations/{id}/cancel (require token) or status=cancelled with token | back/app/main.py |
| Step | Task | Delivered |
|---|---|---|
| 7 | Tables canvas: style for status=reserved, show “Reserved” and reservation summary in side panel | front tables-canvas |
| 8 | Reservations list view: by date (default today), filters (status, search by phone/name), actions Edit / Cancel / Seat / Finish | front new view + API calls |
| 9 | Create / Edit reservation form (staff): date, time, party size, name, phone; POST/PUT /reservations | front |
| 10 | Seat modal: list available tables (capacity ≥ party_size), confirm → PUT /reservations/{id}/seat | front |
| Step | Task | Delivered |
|---|---|---|
| 11 | Public booking page: tenant context, form (date, time, party size, name, phone); submit → POST /reservations; success screen with reservation details and token-based “View / Cancel” link | front (public route) |
| 12 | View / Cancel reservation page: input token (from link or manual); GET by token; show reservation; “Cancel” → confirm → cancel API | front (public route) |
| 13 | Routing: tenant-aware URL for booking (e.g. /book or /t/{tenant}/book) reusing existing tenant resolution | front routing + tenant |
| Step | Task | Delivered |
|---|---|---|
| 14 | Validation: past date/time on create/update; seat_count ≥ party_size when seating; optional opening hours later | back + front |
| 15 | Optional: link order to reservation (reservation_id on order) when seating; optional notifications later | back |
Summary: Staff and web end-user reservation are both essential. Backend delivers staff CRUD + seat/finish + table status “reserved,” and public create + token-based view/cancel. Frontend delivers staff list/create/edit/seat/finish and table canvas “reserved,” plus public booking form and view/cancel by token.
| Item | Status | Notes |
|---|---|---|
| Reservation permissions | To do | Add RESERVATION_READ and RESERVATION_WRITE to back/app/permissions.py; assign to owner, admin, and staff (same pattern as TABLE_READ / TABLE_WRITE). |
| Tenant for public booking | Decided | Public POST /reservations takes tenant_id in body. No tenant slug in DB today; optional later (e.g. /book/{slug}). |
| Migration naming | Convention | Next timestamp, e.g. back/migrations/20260313000000_add_reservation_table.sql. |
| Public vs staff create | Decided | One POST /reservations: with auth → staff (tenant from user); without auth → public (tenant_id required in body). |
Verdict: Ready for implementation. Start with Step 1 (migration); add the two permissions when implementing Step 3 (staff endpoints).