ArMenu
Multi-tenant QR menus with in-browser AR — and no app to install.

- My role
- Solo developer — API, both React apps, the 3D asset pipeline, delivery and observability.
- Status
- Live
The problem
A printed menu cannot show a guest what a dish looks like, cannot be read in their language, and cannot be changed without a reprint. Every digital answer to that asks the guest to install something, which at a restaurant table is where the idea dies.
What I built
A multi-tenant B2B SaaS. A guest scans the QR code on the table, reads the menu in their own language, and places a dish on the table in augmented reality through model-viewer — in the browser they already have open. Behind it: a .NET 10 API over PostgreSQL, and a separate Node service that turns an uploaded 3D model into optimised GLB, USDZ and poster images.
What it looks like

What the guest sees after scanning the QR code on the table. 
The 3D stack loads only when a dish with a model is opened — guests who never use AR never pay for it. 
Per-table QR cards, printed from the dashboard — the table number travels with the scan.
Architecture
Guest & dashboard apps
Two static apps on a CDN. The guest app has to open instantly on mobile data; the dashboard is where owners edit their own menus.
- React 19
- TanStack Router + Query
- Vite
- React Aria
API
Every rule in one place, behind a committed OpenAPI contract that generates the clients' TypeScript types.
- .NET 10
- Minimal APIs
- CQRS
- JWT per tenant
Persistence
Tenant isolation enforced by the database itself, not by remembering to filter.
- PostgreSQL 18
- EF Core 10
- Row-level security
Asset pipeline
Turns an uploaded model into Meshopt + WebP GLB, a USDZ for iOS Quick Look, and poster images.
- Node.js
- glTF Transform
- meshoptimizer
- headless Chrome
Decisions & trade-offs
Row-level security in the database, not in the query layer
A tenant filter that lives in application code is one forgotten WHERE clause away from showing one restaurant another's menu. Pushing isolation into Postgres makes the leak impossible rather than unlikely.
Clean Architecture enforced by a test, not by discipline
Architecture tests fail the build if a source dependency points outward. A layering rule that is only written in a README is a layering rule that erodes.
WebAR instead of a native app
A diner standing at a table will not install an app to look at a menu. model-viewer loads on demand, so the guests who never open AR never pay for it.
A separate service for 3D assets
Mesh optimisation is slow and CPU-bound. Keeping it out of the API means one large upload can never make a menu request wait behind it.
Outcome
Restaurants change their own menus; guests see a dish before they order it. Neither side installs anything.
Measured
- Lighthouse (perf / a11y / BP / SEO)
- 98 / 100 / 100 / 100
- Total blocking time · layout shift
- 0 ms · 0
- Startup JavaScript (gzip)
- 117 KiB
- 3D stack, loaded on demand (gzip)
- 291 KiB