Architecture
Larderline
A solution-architecture summary — how the system is designed, and why. Verified from the source code.
- Type of system
- Multi-tenant SaaS (recipe costing & allergens)
- Users
- Independent UK restaurants
- Hosting
- United Kingdom
- Main stack
- React + TypeScript, Django REST, PostgreSQL
- Status
- Live (free + paid plans)
- Built for
- Moanv's own product
The problem
Costing a menu in a spreadsheet loses money three ways: scaled recipes drift because volume-to-weight conversions treat grated and diced the same; ingredient prices go stale; and the allergen matrix is a Word doc that's out of date the moment a supplier changes — a £5,000-plus risk under Natasha's Law.
How it fits together
Users
Restaurant owners & chefs
Front end
React 18 + TypeScript (Vite, MUI)
Tenant boundary
Backend / API
Django + DRF (Gunicorn, nginx)
Database
PostgreSQL
One shared database; every row carries an owner, and team sharing expands a user to their team's IDs at query time.
External services
- Stripe — billing
- Microsoft Graph — email
- AI providers (bring-your-own-key)
- Listmonk — marketing email
- Chatwoot — support
Tech stack
Front end
- React 18, TypeScript, Vite, MUI
- TanStack Query, Recharts
- jsPDF / html2canvas (client PDF)
Backend
- Python 3.11, Django 4.2, Django REST Framework
- Gunicorn (systemd)
- reportlab / pypdf (server-side PDF)
Database
- PostgreSQL
Hosting
- United Kingdom
- Single Ubuntu VPS, nginx, systemd, deployed via Azure DevOps over SSH
Third-party
- Stripe (billing)
- Microsoft Graph (email)
- Listmonk (marketing email)
- Chatwoot (support)
- AI providers — bring-your-own-key
Key design decisions
Never let the model say a dish is free from an allergen
Why: The allergen tool forces all 14 UK allergen keys onto every dish and collapses anything missing or unrecognised to ‘check’ — never to a negative. A build test fails if banned words like ‘free from’ reach the output.
Trade-off: The tool is deliberately conservative: it says ‘check’ rather than reassure.
A density-aware scaling engine
Why: Volume-to-weight differs by preparation — grated carrot is 55g per 100ml, diced is 115g. A lookup of 147 densities and 59 prep forms keeps a scaled recipe true from 2 to 200 portions.
Trade-off: The density data has to be maintained.
Role hierarchy plus a permission matrix
Why: owner / head_chef / chef / viewer mapped to resources and HTTP verbs, enforced by DRF permission classes.
Trade-off: More permission design up front.
Field-level encryption for users' own AI keys
Why: Bring-your-own-key credentials are encrypted (Fernet/AES) before storage, so the database alone doesn't yield them.
Trade-off: Encryption is keyed from the app secret, which must be protected.
Security & data protection
- Hosted in the United Kingdom.
- TLS 1.2/1.3 with HSTS, nosniff, X-Frame-Options DENY and X-XSS-Protection at nginx.
- JWT authentication (email + password) plus Google and Microsoft sign-in; email verification and password reset.
- Role-based access (owner / head_chef / chef / viewer) via a permission matrix.
- Users' own AI keys encrypted at rest (Fernet / AES).
- Rate limits (per-user daily, newsletter, demo and allergen tools) plus honeypot fields on public forms.
- An activity log records who did what to which resource.
- Secrets live in the environment only; production refuses to boot without its key and database password.
Built to hold up
- Performance
- A React single-page app with a Django API and server-side PDF generation.
- Availability
- A single UK VPS, systemd-managed.
- Scalability
- Owner-scoped tenancy on shared PostgreSQL; server-side plan limits on recipes, AI, scaling, ingredients and seats.
What we'd do next
- Improve mobile load time so it's quick on the pass, not just on a laptop.
- Add supplier price feeds so ingredient costs update themselves.
- Broaden allergen and costing exports to more delivery platforms.