A-101 · Case study · Section 01
Banh Mi Daily
Ordering & store-operations platform
30-second card
- Role
- Full-Stack — end-to-end FE, BE, DB & self-managed deployment
- Surfaces
- Website · Self-service kiosk · Tablet POS · Admin dashboard
- Period
- 06/2026 — Present
- Stack
- Nuxt 4 · Vue 3 · TypeScript · Feature-Sliced Design · TanStack Query · Zod · Ant Design Vue · PWA · Turborepo · NestJS · Prisma · PostgreSQL · JWT · RBAC · Mollie · Webhooks · Docker · Nginx · Cloudflare Pages/R2 · GitHub Actions
- Links
- banhmidaily.nl ↗admin.banhmidaily.nl ↗
Screens

Website: the menu, with the cart and the language selector. 
Self-service kiosk: picking dishes, with the basket at the bottom. 
Tablet POS: orders in progress. Each card shows the order state (Being prepared) and the payment state (Paid · Cash) side by side. 
Admin dashboard: every order has a Status and a Payment status column — the two axes of the order state machine (D1).
Problem
A store needed online ordering and in-store operations on one platform — a website, a self-service kiosk, a tablet POS and an admin dashboard — designed, built and run by a single developer.
Constraints
- Sole developer across frontend, backend, database and deployment
- Concurrent checkout without race conditions
- Real payments with iDEAL and cards
- Trilingual customer-facing website
- Receipt printing on ESC/POS hardware from the browser
Section 01 · decisions in place
- D1
Dual-axis order state machine
Order state is modelled as an explicit state machine and enforced by the API.
Trade-off Every status change goes through an explicit transition, so a new case needs a new transition rather than a quick field update. In exchange, impossible combinations — COMPLETED before READY — cannot be written.
- D2
Idempotency keys + advisory locks
Eliminate race conditions when checkouts arrive concurrently.
Trade-off Each checkout takes a lock and writes a ledger row before the order exists. A unique constraint alone would be simpler, but it only rejects the duplicate at the end, after both requests have done the work.
Try it — send the same order twice → - D3
Server-authoritative pricing & VAT
Totals and VAT are computed by the API, never trusted from a client surface.
Trade-off Every surface asks the API for the total instead of computing it, so price rules live in one place but each total is a round trip. In exchange, no kiosk, POS or website can change what a customer pays.
- D4
Verified, idempotent payment webhooks
Signature-verified Mollie webhooks drive payment-status reconciliation and order confirmation.
Trade-off A payment is final only when Mollie's signed webhook arrives, so the order waits in PENDING rather than trusting the browser's redirect. In exchange, each payment is confirmed exactly once, even when Mollie retries the webhook.
- D5
JWT refresh-token rotation + RBAC
Sessions rotate refresh tokens; access is role-based.
Trade-off Rotating refresh tokens means storing and revoking them on the server, and every endpoint declares the roles it accepts. In exchange, a stolen refresh token stops working after one use, and staff reach only what their role allows.
- D6
Validated API boundary
TanStack Query paired with Zod validation where data enters the frontend.
Trade-off Zod schemas are kept alongside the API's types. In exchange, data that does not match fails at the boundary with a clear error, instead of breaking a screen later.
- D7
Image pipeline
Uploads are processed with Sharp and stored on Cloudflare R2.
Trade-off Uploads go through a processing step before they are stored, instead of keeping the original file. In exchange, every surface serves images that are already sized and compressed.
Delivery · plan at ▽ −11.00
- GitHub Actions CI runs lint, typecheck, tests and PostgreSQL E2E
- API runs in Docker behind Nginx on a VPS
- Site and images served from Cloudflare Pages and R2