NexTicket — A Multi-Tenant SaaS Platform for Restaurant Operations, Built with Elixir & Phoenix

Running a restaurant must be chaotic. Orders get lost between the counter and the kitchen. Waiters waste time walking back and forth. Customers wait too long to place an order. And if you're managing more than one location, multiply that chaos by ten.
NexTicket is a real-time restaurant management platform I built from scratch with Elixir, Phoenix LiveView, and PostgreSQL — designed to eliminate the friction between customers, counter staff, and the kitchen.
In this article, I'll walk you through the architecture, the technical decisions, and the lessons learned while building it.
The Problem
Most restaurant management tools fall into one of two camps: overpriced enterprise software with features no one uses, or fragile point-of-sale terminals that crash during the dinner rush.
I wanted something different:
- Customers scan a QR code at their table and order from their own phone — no app download required
- Counter staff see every table's status in real-time and validate incoming orders
- Kitchen staff get a live display of what to prepare and in what order
- Restaurant owners manage their menu, tables, and staff from one dashboard
- Everything updates instantly — no page refreshes, no polling

How It Works
The order flow is designed around how restaurants actually operate:
- A customer sits down and scans the QR code on their table
- The counter is instantly notified — "Table 5 has guests"
- Counter staff activates the session and the customer can start browsing the menu
- The customer browses the menu, customizes items, and places an order — all from their phone
- The counter reviews and confirms the order, then sends it to the kitchen
- The kitchen prepares the food using a live Kitchen Display System (KDS)
- When it's ready, the kitchen marks it done — the counter is notified to deliver
- Order complete
Every step happens in real-time. No paper tickets. No shouting across the kitchen.

Architecture: Multi-Tenancy with PostgreSQL Schemas
NexTicket is a true multi-tenant SaaS platform. Each restaurant gets its own isolated PostgreSQL schema, not just a tenant_id column on every table.
┌──────────────────────────────┐
│ Public Schema │
│ accounts, tenants, plans, │
│ subscriptions │
└──────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ tenant_ │ │ tenant_ │ │ tenant_ │
│ pats_pizza_ │ │ sushi_bar_ │ │ cafe_luna_ │
│ a7b3c9d2 │ │ f2e8d1a4 │ │ c5b7e3f1 │
│ │ │ │ │ │
│ staff, menu, │ │ staff, menu, │ │ staff, menu, │
│ tables, │ │ tables, │ │ tables, │
│ orders, ... │ │ orders, ... │ │ orders, ... │
└──────────────┘ └──────────────┘ └──────────────┘
Why schemas instead of row-level tenancy?
| Aspect | Schema Isolation | Row-Level (tenant_id) |
|---|---|---|
| Data isolation | Complete — impossible to query another tenant's data | Application-level — one missing WHERE clause = data leak |
| Performance | Fast — no JOIN on tenant_id for every query | Slower — every query needs tenant filtering |
| Compliance | Schema-level security boundaries | Relies on application correctness |
| Per-tenant backups | backup individual schemas |
Each restaurant is identified by its subdomain. A request to pats-pizza.nexticket.com automatically sets the database prefix to the correct tenant schema. All subsequent queries are scoped — there's no way to accidentally leak data between restaurants.
Real-Time, Everywhere: Phoenix PubSub
The magic of NexTicket's real-time experience comes from Phoenix PubSub combined with LiveView.
When a customer places an order, the counter dashboard updates instantly. When the kitchen marks a dish as ready, the counter sees it immediately. No WebSocket boilerplate — Phoenix handles it all.
But in a multi-tenant system, you need to be careful: if Restaurant A creates an order, Restaurant B's kitchen shouldn't see it. Every PubSub topic is prefixed with the tenant's schema name:
tenant_pats_pizza_a7b3c9d2:orders
tenant_sushi_bar_f2e8d1a4:orders
This ensures complete message isolation between tenants while still leveraging Phoenix's lightweight PubSub system.

The Customer Experience
When a customer scans the QR code at their table, they land on a clean, mobile-first menu — no app to download, no account to create.
They can:
- Browse categories with smooth scrolling navigation
- View item details with full-screen images and descriptions
- Build a cart with per-item notes ("no onions", "extra spicy")
- Add order-level notes ("birthday dinner — surprise cake at the end")
- Track their order status in real-time as it moves through the kitchen
The entire experience is a single Phoenix LiveView — no page loads, no spinners.



The Admin Dashboard
Restaurant owners get a full management interface:
- Menu Management — Categories and items with drag-and-drop ordering, image uploads, and pricing
- Table Management — Create tables, auto-generate QR codes, monitor active sessions
- Staff Management — Role-based access control (Admin, Staff, Kitchen, Counter)
- Order History — Paginated order history with search and filtering
- Sales Reports — Revenue tracking and popular item analytics
- Audit Trail — Every action is logged — who changed what, and when

Technical Highlights
Performance & Caching
Frequently accessed data (like menu categories and items) is cached with Cachex, using tenant-scoped cache keys. When a menu item is updated, only the relevant cache entry is invalidated — not the entire menu.
Background Jobs
Oban handles background processing — daily sales report emails, session cleanup, and subscription management. Each job carries the tenant context so it can set the correct database schema before executing.
Security
- 15-minute idle session timeout for staff workstations
- Role-based access control at both the router and LiveView level
- Rate limiting on registration and authentication endpoints
- Complete audit trail for all destructive operations
- CSRF protection and XSS prevention built into Phoenix
Subscription System
A flexible subscription tier system with configurable feature limits:
| Feature | Starter | Professional | Enterprise |
|---|---|---|---|
| Tables | 10 | 50 | Unlimited |
| Menu Items | 50 | 200 | Unlimited |
| Staff Users | 3 | 10 | Unlimited |
| Order History | 30 days | 365 days | Unlimited |
| Advanced Analytics | — | ✓ | ✓ |
| Custom Branding | — | ✓ | ✓ |
The Tech Stack
| Layer | Technology |
|---|---|
| Language | Elixir 1.15+ |
| Framework | Phoenix 1.8 with LiveView |
| Database | PostgreSQL 16 |
| Multi-tenancy | PostgreSQL schemas |
| Background Jobs | Oban |
| Caching | Cachex |
| File Storage | Waffle (local dev) / S3 (production) |
| QR Codes | qr_code |
| Pagination | Flop |
| Styling | Tailwind CSS 4 |
Lessons Learned
Schema-based multi-tenancy is powerful but requires discipline. Every background job, every PubSub subscription, every cache key needs to be tenant-aware. Miss one, and you'll get silent data leaks or cross-tenant messages.
LiveView changes how you think about UIs. There's no REST API, no frontend framework, no state management library. The server is the state. It's remarkably productive once you embrace it.
Component extraction matters. The counter dashboard started as a 1,650-line LiveView. Breaking it into focused components (session cards, order cards, notification panels) made it maintainable without sacrificing the real-time experience.
Elixir's concurrency model is a perfect fit for restaurant ops. Each table session, each order, each kitchen display — they're all independent processes that need to communicate. That's exactly what the BEAM was built for.
What's Next
NexTicket is feature-complete for its core use case. The roadmap includes:
- Translations
- Payment integration (Stripe) for subscription billing
- Production deployment on Fly.io with wildcard subdomain support
- Advanced analytics with historical trend reporting
- Multi-location support for restaurant chains
- More customization for the client