Introduction
This project aims to build a complete restaurant management system that runs entirely on a local Wi-Fi network (LAN).
When a client sits down, they can scan a QR code printed on their table, if they don’t want, the waiter can do it for them and bring a paper menu instead.
The QR code automatically connects to the local web app and encodes the table ID, allowing the system to know which table is placing the order, no internet required.
Everything — from the backend to the web menu — runs inside the restaurant’s private local network.
System Overview
🖥️ Backend (API)
A lightweight backend (FastAPI) serves as the core of the system, hosted locally on a mini-PC or central computer connected to the restaurant’s router.
It manages:
- Tables, orders, and menus
- Staff authentication
- Communication between clients, waiters, and the kitchen
- Storage via an internal database (SQLite for the moment)
🍽️ Frontend for Clients
Clients access the menu through their browser after scanning the QR code (or paper menu if asked).
The system automatically loads the correct table and displays a modern, mobile-friendly web interface (React) where clients can:
- Browse dishes
- Customize options
- Place orders directly
No need to install an app, everything runs instantly through the LAN.
👨🍳 Real-Time Communication
To make communication faster and smoother, the system uses WebSockets between:
- The waiters’ devices (phones or tablets)
- The kitchen display
This ensures instant updates: as soon as an order is placed, the kitchen sees it immediately, and when it’s ready, the waiter gets notified in real time without reloading pages.
🧩 Easy Deployment with Docker
Everything is containerized using Docker for simplicity.
The restaurant owner just needs to:
- Install Docker on a local computer or small server.
- Run one command (
docker-compose up).
- The system automatically sets up:
- Backend API
- WebSocket server
- Database
- Frontend web app
- Insert the menu items and basic items in the database as well as the staff members and tables (QR codes are created automatically).
The goal is to make installation as easy as plugging in a coffee machine ☕ fully automated and ready to go.
🧩 Database Schema (ER Diagram)
Below is the complete Entity–Relationship Diagram showing how users, orders, and items connect — including analytical extensions like costs, promotions, and inventory logs.
erDiagram
%% === USERS ===
USERS {
int id PK
string username UK
string hashed_password
string email UK
int age
bool gender "nullable"
enum role "admin, kitchen, client, waiter"
int table_id FK "nullable"
}
%% === TABLES ===
TABLES {
int id PK
int number UK "physical table number"
int capacity
enum status "available, occupied, reserved, cleaning"
enum location_zone "indoor, outdoor, terrace, bar, vip"
datetime reservation_start "nullable"
}
%% === BASIC ITEMS ===
BASIC_ITEMS {
int id PK
string name
float stock
string unit "kg, liters, pieces"
float base_cost
float tax_rate
datetime expiration_date
datetime last_updated
int last_updated_by FK
text description "nullable"
}
%% === MENU ITEMS ===
MENU_ITEMS {
int id PK
string name UK
float price
int stock
enum category "entry, main_course, dessert, beverage"
bool available
datetime created_at
text description "nullable"
}
%% === MENU ITEM COMPONENTS ===
MENU_ITEM_COMPONENTS {
int menu_item_id PK,FK
int basic_item_id PK,FK
float quantity_required
}
%% === ORDERS ===
ORDERS {
int id PK
int user_id FK "nullable for guest orders"
int table_id FK
enum status "pending, confirmed, preparing, ready, served, completed, cancelled"
datetime created_at
datetime finished_at "nullable"
text specifications "nullable"
float total_amount
float discount_applied
enum payment_method "cash, card, mobile, voucher, pending"
string promo_code FK "nullable"
int num_customers "number of people for this order"
}
%% === ORDER ITEMS ===
ORDER_ITEMS {
int order_id PK,FK
int item_id PK,FK
int quantity
float item_price
float item_cost
}
%% === PROMOTIONS ===
PROMOTIONS {
int id PK
string code UK
text description
float discount_percentage
enum target_category "nullable"
int target_menu_item "nullable"
datetime start_date
datetime end_date
}
%% === INVENTORY LOGS ===
INVENTORY_LOGS {
int id PK
int user_id FK
int item_id FK
datetime timestamp
float stock_change
enum reason "initial_stock, restock, sale, waste, theft, correction, return, sample"
string notes "nullable"
}
%% === STAFF SHIFTS ===
STAFF_SHIFTS {
int id PK
int user_id FK
datetime shift_start
datetime shift_end
enum role "waiter, kitchen"
}
%% === DAILY LOGS ===
DAILY_LOGS {
int id PK
date log_date UK
int total_customers
float total_revenue
float total_expenses
float worked_time
}
%% === MONTHLY OVERVIEW ===
MONTHLY_OVERVIEW {
int id PK
date month_start "e.g., 2025-11-01"
string category "revenue, food_cost, staff_cost, electricity, rent, taxes, etc."
float amount "positive for income, negative for expense"
text notes "optional"
}
%% === MONTHLY ITEM STATS ===
MONTHLY_ITEM_STATS {
int id PK
int menu_item_id FK
date month_start
int quantity_sold
float revenue_generated
float total_item_cost
float avg_margin
}
%% === RELATIONSHIPS ===
USERS ||--o{ ORDERS : "places/manages"
TABLES ||--o{ USERS : "seats"
TABLES ||--o{ ORDERS : "serves at"
ORDERS ||--|{ ORDER_ITEMS : "contains"
MENU_ITEMS ||--o{ ORDER_ITEMS : "ordered as"
MENU_ITEMS ||--|{ MENU_ITEM_COMPONENTS : "composed of"
BASIC_ITEMS ||--o{ MENU_ITEM_COMPONENTS : "ingredient in"
USERS ||--o{ BASIC_ITEMS : "last updated by"
USERS ||--o{ INVENTORY_LOGS : "performs change"
BASIC_ITEMS ||--o{ INVENTORY_LOGS : "tracked in"
PROMOTIONS ||--o{ ORDERS : "applied to"
DAILY_LOGS ||--o{ STAFF_SHIFTS : "includes staff shifts"
DAILY_LOGS ||--o{ ORDERS : "summarizes orders of the day"
MONTHLY_ITEM_STATS ||--|| MENU_ITEMS : "analyzes"
MONTHLY_ITEM_STATS ||--|| MONTHLY_OVERVIEW : "belongs to month"
DAILY_LOGS ||--o{ MONTHLY_OVERVIEW : "aggregated into"
🔄 System Dataflow
Below is the request and role-based dataflow for both public clients and authenticated users.
flowchart TD
%% === Public client flow ===
subgraph GuestClient["Guest Client (no login)"]
GC["Client (Guest)"] -->|"GET /menu"| PublicAPI
GC -->|"POST /orders"| PublicAPI
end
%% === Unified Authenticated flow ===
subgraph AuthFlow["Authenticated Users (Client or Staff)"]
Login["User logs in (Client / Waiter / Kitchen / Admin) -> POST /token"] --> AuthEndpoint["Auth Endpoint"]
AuthEndpoint --> UsersDB["Users Table"]
AuthEndpoint --> JWT["JWT Token Created"]
JWT --> API["Authenticated API (profile, orders, management, etc.)"]
API --> RoleCheck["Role-based Access"]
RoleCheck --> Client["Client: access personal data & order history"]
RoleCheck --> Waiter["Waiter: manage orders & receive WS updates"]
RoleCheck --> Kitchen["Kitchen: update order statuses"]
RoleCheck --> Admin["Admin: manage menu & users"]
end
%% === Database ===
DB["(Orders, Items, Tables, Users)"]
SPDB["(Items, Tables)"]
%% === Roles branching ===
RoleCheck --> DB
%% === Shared backend access ===
PublicAPI --> SPDB
🔒 Security and Open Questions
Even though the system runs locally, several important security questions arise:
- Payment Integration:
Should payments be handled within the same local system, or should they be decoupled (handled by an external terminal or service)?
- If decoupled, how can we securely record the payment status in the local database?
- Would an API endpoint for “payment success” be enough, or is a more robust protocol needed?
- Network Isolation:
Is it truly safe to host the backend on a local Wi-Fi network accessible to clients?
- Should the network be split into two VLANs, one for staff and one for customers?
- How can we prevent malicious access or API abuse if a client tries to inspect network traffic man-in-the-middle attacks?
- Data Integrity and Reliability:
What happens if the local server restarts during peak hours?
- Should there be automatic data backup or replication to a small cloud service?
- How to keep everything purely local but still safe?
- API Design:
The API structure currently separates routes for /products, /orders, /kitchen, and /waiters.
- Is this modular structure appropriate, or should all roles share a common
/orders endpoint with role-based filtering?
- How could WebSocket channels be organized? Per role, per table, or globally?
I’d love to hear feedback from developers and system designers on these architectural and security choices.
Your insights could help refine this concept into a production-ready, open-source solution for small restaurants. Thank you guys :D
Source Code
👉 ZeCo Backend on GitHub
👉 ZeCo Dashboard on Streamlit
José ALVES, 2025.