Restaurant POS and ordering infrastructure — self-hosted, multi-tenant, real-time.
A point-of-sale system with a customer-facing menu, table management, reservations, and Stripe payments. Staff use the Angular admin; customers order via QR codes and pay at the table. You keep full control of your data and deployment.
POS2 is built for restaurants and venues that want:
- One place for everything — Orders, tables, reservations, menu, and payments in a single stack.
- Customer ordering without apps — Guests scan a table QR code, browse the menu, place orders, and pay with Stripe. Optional table PIN keeps ordering secure.
- Real-time updates — Order status (pending → preparing → ready → delivered → paid) flows to staff and customers over WebSockets.
- Multi-tenant from day one — Each restaurant (tenant) has isolated data, settings, and Stripe configuration.
- Self-hosted — Run on your own server or local network; no vendor lock-in.
The frontend is Angular; the backend is FastAPI with PostgreSQL and Redis. All major flows are implemented and documented (see ROADMAP.md and the docs/ folder).
The staff dashboard gives quick access to Catalog, Reservations, Kitchen display, Reports, and more.
More screenshots (orders, kitchen display, reports, customer menu) are listed in docs/screenshots/README.md.
| Area | What's included |
|---|---|
| Orders | Full lifecycle (pending → preparing → ready → delivered → paid). Session-based orders per browser. Item-level status; partial delivery; order modification and cancellation before delivery; soft delete with “Show removed items” in staff UI. Print invoice and Print Factura (with optional billing customer and tax breakdown) open the browser print dialog. See docs/0017-billing-customers-factura.md. |
| Tax (IVA) | Tax-inclusive pricing with per-tenant tax rates (name, rate %, validity). Default tax in Settings; product-level tax override. Order items store applied tax snapshot for invoice breakdown. |
| Billing customers (Factura) | Register customers that need a tax invoice with company details (name, company, CIF/tax ID, address, email, phone). List and search at /customers; from Orders, Print Factura lets you select a customer and print an invoice with “Bill to” block; optionally save the customer on the order. |
| Customer menu | Browse menu, cart, place order, order history. Optional “immediate payment required” (checkout auto-opens after placing order). |
| Kitchen display | Dedicated full-screen view at /kitchen: large order cards, auto-refresh and WebSocket updates, optional sound on new orders. Read-only; same access as Orders. See docs/0015-kitchen-display.md. |
| Reports | Sales & revenue at /reports (owner/admin): date range, summary (total revenue, order count, average payment per client), reservation count and by source (public/staff), by product/category/table/waiter, charts, CSV/Excel export. See docs/0016-reports.md. |
| Payments | Stripe (online card payment at table); cash and card terminal (dataphone) (staff marks order as paid and records method). Optional immediate payment required (checkout opens right after placing order). Per-tenant Stripe keys and currency. |
| Tables | Table management, QR codes, canvas view. Table activation and 4-digit PIN so only present guests can order; PIN rate limiting via Redis. |
| Reservations | Staff: list, create, edit, seat, finish, cancel at /reservations. Client notes (from the customer at booking) and owner notes (internal staff notes). Client technical info (IP, user-agent, browser fingerprint, screen size) is recorded for public bookings and visible to staff. No-show: mark no-shows and send reminders by email and/or WhatsApp (when Twilio is configured). Public: book at /book/:tenantId, view/cancel at /reservation?token=.... Table status: available / reserved / occupied. |
| Real-time | WebSocket updates for order status; token-based WS auth (/ws-token). |
| i18n & currency | Multiple UI languages (e.g. en, es, ca, de, zh-CN, hi); backend localized messages; per-tenant currency (EUR, USD, MXN, etc.). |
| Multi-tenant | Isolated data per tenant; first user becomes owner; configurable roles (owner, admin, kitchen, bartender, waiter, receptionist) and permissions (e.g. reservation read/write). |
| Products & images | Staff manage products at /products. On deploy, demo products are linked to catalog provider products so the Products page shows images (beer/pizza/wine import); first load of /products backfills image URLs. |
| Provider portal | Suppliers register at /provider/register, log in at /provider/login, and manage their catalog at /provider (tile/list view, search, add/edit/delete products, company details). See docs/0014-provider-portal.md. |
| Tenant branding (public) | Per-tenant background colour (hex, e.g. RAL5002 Azul) and header background image for book, menu, and reservation-view pages. Set in Settings → Business profile; optional dark overlay when a header image is used. See docs/0028-tenant-public-branding.md. |
Planned but not yet implemented: batch order operations, and stricter “must pay before continuing” flow. See ROADMAP.md.
- Frontend: Angular 21+ (SPA)
- Backend: FastAPI, SQLModel ORM
- Database: PostgreSQL 18
- Cache / pub-sub: Redis 7
- Real-time: WebSocket bridge (custom service)
- Payments: Stripe
- Deployment: Docker Compose, HAProxy
The quickest way to try POS out is to head over to https://satisfecho.de/ and set up your restaurant.
- Docker and Docker Compose
- Git
-
Clone the repository
git clone https://github.com/satisfecho/pos cd pos -
Configure environment
cp config.env.example config.env
For local development the defaults are fine. For production or a custom domain, set
API_URL,WS_URL,CORS_ORIGINS, andSECRET_KEY. See docs/0004-deployment.md. -
Start all services
- Local (development):
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file config.env up -d - Production (e.g. amvara9):
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file config.env up -d
- Local (development):
-
Find the app URL
Rundocker compose -f docker-compose.yml -f docker-compose.dev.yml ps(or with.prod.ymlif you used prod) and check the PORTS column for thehaproxyservice (e.g.0.0.0.0:4202->4202/tcpor0.0.0.0:4203->4202/tcp). The host port (4202 or 4203) is your app URL.- App (recommended): http://localhost:4202 (or the port shown for haproxy)
- API docs: http://localhost:4202/api/docs
- Health: http://localhost:4202/api/health
-
Create an account
There is no pre-seeded user. Open http://localhost:4202/register (use your actual port), enter tenant name, email, and password. The first user becomes the tenant owner. Then sign in at the main URL.
| Purpose | URL |
|---|---|
| Unified app (frontend + API via HAProxy) | http://localhost:4202 |
| API docs (Swagger) | http://localhost:4202/api/docs |
| Health check | http://localhost:4202/api/health |
| DB health | http://localhost:4202/api/health/db |
| Public menu (example) | http://localhost:4202/menu/{table_token} |
| Public booking | http://localhost:4202/book/{tenantId} |
| Provider login | http://localhost:4202/provider/login |
| Provider dashboard | http://localhost:4202/provider |
| Kitchen display | http://localhost:4202/kitchen |
| Reports (owner/admin) | http://localhost:4202/reports |
| Customers (Factura) | http://localhost:4202/customers |
| Dashboard (staff) | http://localhost:4202/dashboard |
If your frontend port is different (e.g. 4203), replace 4202 with that port. See AGENTS.md for how to detect the port and debug with logs.
Key variables in config.env (see config.env.example for the full list):
| Variable | Description | Required |
|---|---|---|
SECRET_KEY |
JWT signing; change in production | Yes |
REFRESH_SECRET_KEY |
Refresh token signing; must differ from SECRET_KEY |
Yes |
API_URL |
Backend base URL used by frontend (e.g. http://localhost:4202/api) |
For custom domain |
WS_URL |
WebSocket URL (e.g. ws://localhost:4202/ws) |
For custom domain |
CORS_ORIGINS |
Allowed frontend origins (comma-separated) | Yes (production) |
POSTGRES_* / DB_* |
Database connection | Yes |
STRIPE_CURRENCY |
Fallback currency if tenant has none | Optional |
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_WHATSAPP_FROM |
Optional; when set, reservation reminders can be sent via WhatsApp (in addition to email) | Optional |
DEFAULT_PHONE_COUNTRY |
ISO country code (e.g. ES, DE) for normalizing reservation phone numbers to E.164 |
Optional (default ES) |
Stripe keys are configured per tenant in Settings in the admin UI. Settings also include business profile, contact (phone, email, address, Tax ID, CIF), opening hours, and payment options. For deployment on a domain or IP, see docs/0004-deployment.md.
| Document | Description |
|---|---|
| ROADMAP.md | Implemented vs planned features; rate limiting and security roadmap |
| CHANGELOG.md | Release notes and unreleased changes |
| AGENTS.md | How to find the app port and view logs (for developers/agents) |
| docs/README.md | Index of all documentation (deployment, email, features, plans, testing) |
| docs/0008-order-management-logic.md | Order lifecycle, session rules, status reset |
| docs/0007-implementation-verification.md | What’s implemented vs Phase 4 (batch, audit, etc.) |
| docs/0010-table-reservation-implementation-plan.md | Reservations design and backend |
| docs/0011-table-reservation-user-guide.md | URLs and flows for staff and public booking |
| docs/0019-no-show-implementation-plan.md | No-show status and reminder emails: plan and implementation guide |
| docs/0024-whatsapp-reminder-notes.md | WhatsApp reservation reminder: design, Twilio config, E.164 phone normalization |
| docs/0009-table-pin-security.md | Table activation and PIN validation |
| docs/0012-translation-implementation.md | i18n (frontend + backend + DB content) |
| docs/0004-deployment.md | Domain/IP deployment and env vars |
| docs/0002-customer-features-plan.md | Customer accounts: registration, login, MFA, order history, invoices |
| docs/0005-email-sending-options.md | Email configuration options |
| docs/0013-verification-alternatives.md | Verification flow alternatives |
| docs/0014-provider-portal.md | Provider registration, login, and catalog management |
| docs/0015-kitchen-display.md | Kitchen display: full-screen view, auto-refresh, optional sound |
| docs/0017-billing-customers-factura.md | Billing customers (Factura): register company details, search, print invoice with “Bill to” |
Browser → Frontend (Angular) → Backend (FastAPI) → PostgreSQL
↓
Redis (pub/sub)
↓
WebSocket bridge → Browser (real-time)
Services (Docker Compose):
- haproxy — Single entry point; routes
/to frontend,/apito backend,/wsto WebSocket bridge. Port fromFRONTEND_PORT(default 4202). - front — Angular SPA (dev server in container).
- back — FastAPI app (Uvicorn).
- ws-bridge — WebSocket server; subscribes to Redis and pushes order updates.
- db — PostgreSQL 18.
- redis — Cache and pub/sub.
- Port:
docker compose -f docker-compose.yml -f docker-compose.dev.yml ps→ PORTS forhaproxy→ use that host port in the browser. - Logs:
docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f(all); same-flist withlogs --tail=50 back,logs --tail=80 front,logs --tail=30 haproxy.
See AGENTS.md for more detail.
Migrations live in back/migrations/ and run automatically on backend startup.
- Apply manually:
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec back python -m app.migrate - Check pending:
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec back python -m app.migrate --check - New migration: Use timestamped names, e.g.
back/migrations/YYYYMMDDHHMMSS_description.sql. Seeback/migrations/README.md.
Do not edit existing migration files; add a new migration to change schema.
- Frontend and backend both reload on code changes when run via Docker Compose.
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file config.env down- Languages: English, Spanish, Catalan, German, Chinese (Simplified), Hindi (see
front/public/i18n/*.json). - Currency: Per-tenant (e.g. EUR, USD, MXN, INR, CNY, TWD).
- Language picker: In admin sidebar and on the public menu.
- API: Use
?lang=es(or other code) for localized API messages.
See docs/0012-translation-implementation.md.
- Staff: Sign in → Reservations in the sidebar. List, create, edit, seat at a table, finish, or cancel. Tables canvas shows status Reserved (amber) when a reservation is assigned.
- Public: Book at
/book/:tenantId(e.g.http://localhost:4202/book/1). After booking, use the link to view or cancel at/reservation?token=.... No login required.
Details: docs/0011-table-reservation-user-guide.md.
For a custom domain or IP, set in config.env:
API_URLandWS_URLto your backend base URL (usehttps://andwss://for production).CORS_ORIGINSto your frontend origin(s).
Then restart: docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file config.env up -d.
Full guide: docs/0004-deployment.md.
- Done: Order management, reservations, table PIN, Stripe, WebSocket, i18n, deployment docs. See ROADMAP.md for the full list.
- Planned: Order Phase 4 (batch, audit, item replacement), optional stricter “immediate payment” enforcement. Rate limiting and security hardening are described in ROADMAP.md.
- Production: Set a strong
SECRET_KEYandREFRESH_SECRET_KEYinconfig.env. - CORS: Set
CORS_ORIGINSto your real frontend origin(s); avoid*in production if possible. - Database: Use strong credentials; do not commit
config.env. - Stripe: Use live keys in production and configure them per tenant in Settings.
- Rate limiting: Not yet implemented for most endpoints; PIN attempts are rate-limited. See ROADMAP.md for the planned strategy.
| Issue | What to try |
|---|---|
| Services won’t start | Check port conflicts; ensure config.env exists and is valid; run docker compose -f docker-compose.yml -f docker-compose.dev.yml logs. |
| Frontend can’t reach API | Confirm API_URL and WS_URL match how the browser reaches the app (e.g. through HAProxy). Check CORS and browser console. |
| Wrong port | Run docker compose -f docker-compose.yml -f docker-compose.dev.yml ps, find the host port for haproxy, and open that URL (e.g. http://localhost:4202). |
| DB connection errors | Ensure db is healthy (docker compose -f docker-compose.yml -f docker-compose.dev.yml ps); with Compose, use DB_HOST=db. Check credentials in config.env. |
More: docs/0004-deployment.md and AGENTS.md.
