← Back to section 01ENVITRI.DEV · SECTION A–A

A-101 · Case study · Section 01

Banh Mi Daily

Ordering & store-operations platform

Key plan · section 01 of 8

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

Screens

  • Banh Mi Daily website menu: best sellers and Vietnamese bánh mì cards with prices, a cart button showing 3 items and an English language selector.
    Website: the menu, with the cart and the language selector.
  • Kiosk screen titled Pick your dishes: a grid of dishes and drinks with prices, category tabs, and a Go to basket bar showing 1 item for €2.95.
    Self-service kiosk: picking dishes, with the basket at the bottom.
  • Tablet POS in dark mode: order cards, each marked Being prepared, Paid and Cash, with Ready, Receipt, Cancel & refund and Refund buttons.
    Tablet POS: orders in progress. Each card shows the order state (Being prepared) and the payment state (Paid · Cash) side by side.
  • Admin orders table with order code, creation time, channel, items and total, and separate Status (Expired, Cancelled) and Payment status (Payment failed, Not paid) columns.
    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

Section through Banh Mi Daily: each level it reaches, the parts on it, and the decision tags D1 to D7.
  1. 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.

  2. 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 →
  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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

PLAN AT ▽ −11.00 · DELIVERY · BANH MI DAILY · 1:50PROPERTY LINE · SELF-MANAGEDBrowser4 client surfacesCloudflare Pagesstatic site hostingCloudflare R2images via SharpNginxreverse proxyNestJS API · Dockerorders · VAT · authPostgreSQL · Prismahost not documentedMollieiDEAL & cardsGitHub Actionslint · test · E2E※ Schematic of documented responsibilities. Not live traffic.
  • 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