Open-source QR menu, table service and payments for restaurants, cafés, bars and hotels.
Türkçe README için: README.tr.md
Sofra turns every table into a live QR menu. Guests scan the table's QR code and get the menu in their language; they can call a waiter, request the bill, order from the table and even pay online. Staff see everything land on a live panel the second it happens. No app downloads, no POS hardware, no commission. It runs on your own server.
"Sofra" is Turkish for a set dining table.
- Guest menu: /m/demo?table=1
- Staff panel: /admin ·
admin@sofra.local/sofra123
The demo runs on a free instance: it sleeps after 15 minutes idle (first request takes about a minute to wake it) and resets to seed data when it restarts. Order and pay freely, nothing you do there sticks around.
- Table-tied QR menus: each table gets its own QR; scanning opens the menu already tagged with the table number
- Instant updates: change a price once, every table sees it live
- Multilingual: menu content is translated data (per-language fields), never baked into images; add languages in settings
- Categories & subcategories with photos or emoji, prep times, dietary tags (popular / vegetarian / vegan / spicy / gluten-free)
- Option groups: portions with their own prices, paid extras, sizes, sugar levels
- Live table actions: call waiter & request bill, streamed to the staff panel over SSE
- Shared table ordering: one cart per table, not per phone. Everyone seated together adds from their own device and sees the change on their screen the moment it happens, and the table sends a single order, so the kitchen never receives one party's food as unrelated tickets. Notes, server-side pricing, live order statuses
- Pay at the table: pluggable payment layer; ships with a demo provider, Stripe/iyzico adapters are on the roadmap
- Pay at the table, together or separately: the whole bill in one go, an even share each, or an amount someone types in. Everyone pays from their own phone and the remaining total updates for the whole table at once
- Feature toggles: waiter / bill / ordering / payments can each be switched per venue
- Theming: per-venue brand color, light / dark / auto
- Printable QR sheets: one card per table, ready for lamination
- Scan analytics: scans per day and per table, request/order/revenue counters
- Self-host friendly: Prisma + SQLite in a single file, versioned migrations, single Docker container
Requires Node.js >= 18.18.
git clone <this-repo> sofra && cd sofra
cp .env.example .env
npm install # runs `prisma generate`
npm run db:seed # applies migrations, then seeds a demo venue with a full bilingual menu
npm run dev- Guest menu: http://localhost:3000/m/demo?table=1
- Staff panel: http://localhost:3000/admin with
admin@sofra.local/sofra123
Changed the schema? npm run db:migrate creates and applies a new migration
(npm run db:push is the no-migration shortcut for throwaway experiments).
Every release publishes a multi-arch image:
docker run -d -p 3000:3000 \
-v sofra-data:/app/data \
-e SESSION_SECRET="$(openssl rand -hex 32)" \
ghcr.io/OWNER/sofra:latestMigrations run automatically on start. Set SEED_DEMO=1 on the first run to load
the demo venue. Or use the bundled compose file:
docker compose up --buildThe repo ships a render.yaml blueprint. Point Render at your fork
and it builds the Dockerfile, generates a session secret and boots a free instance.
Any host that runs a container with a persistent volume works the same way: Sofra needs one long-lived process (the live panel holds an SSE connection) and a writable directory for the SQLite file.
- Guest scans the table QR →
/m/{venue}?table=Nopens, menu is rendered in their language. - They browse, order, call, or request the bill → requests are validated and priced server-side.
- Staff sees it live → the panel at
/adminreceives events over Server-Sent Events, tagged with the table number. Nothing to refresh.
prisma/schema.prisma # data model
prisma/migrations/ # versioned SQL migrations
scripts/seed.ts # demo data
src/lib/db.ts # PrismaClient singleton
src/lib/repo.ts # typed data access layer
src/lib/payments/ # payment provider abstraction (mock, stripe skeleton)
src/app/m/[slug]/ # guest menu
src/app/admin/ # staff panel (live, menu editor, tables, analytics, settings)
src/app/api/ # REST + SSE endpoints
See ROADMAP.md for what's next.
| Env var | Default | Description |
|---|---|---|
DATABASE_URL |
file:../data/sofra.db |
Prisma SQLite connection string. Relative paths resolve from prisma/; use an absolute one in containers (file:/app/data/sofra.db) |
SESSION_SECRET |
dev fallback | Sign-in cookie secret, set a long random string |
TABLE_TOKEN_SECRET |
SESSION_SECRET |
Signs the per-table QR tokens. Changing it invalidates printed QR sheets |
BASE_URL |
request host | Used on printable QR sheets |
SEED_DEMO |
unset | Set to 1 to seed the demo venue on container start |
SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD |
admin@sofra.local / sofra123 |
Seed credentials |
Each table's QR link carries a signed token (?t=) derived from TABLE_TOKEN_SECRET. Guest
endpoints reject a table number that arrives without its token, so ordering, waiter calls and
payments need a real scan rather than a guessed number. An unsigned link still renders the
menu, just read-only. Guest writes are rate limited per IP and table, and duplicate open
requests are coalesced per table.
Rate limiting is an in-process fixed window, so it only covers a single instance; the multi-instance
story lands with the Redis event bus in v1.0. Rotating TABLE_TOKEN_SECRET (or SESSION_SECRET,
which it falls back to) invalidates printed QR codes, so reprint them from Tables → Print QR sheet.
Issues and PRs are welcome. Keep PRs focused, describe the venue scenario you're solving, and add seed data when a feature needs it.