Case study 02 · 2026

Brewline

A better way to order. From both sides of the counter.

Role
Solo — proposal, design, engineering, deployment
Status
Live demo
Stack
React · TypeScript · Hono · Cloudflare Workers · Cloudflare D1 · Stripe Checkout · Vitest
Links
Brewline: 'Mobile ordering a café can actually keep up with', with the customer app open on a phone.

Overview

While working at a corporate café I wrote a proposal to fix the mobile ordering app customers used. It wasn't adopted, so I built the whole thing: an installable customer app, a barista display and a manager console on Cloudflare Workers and D1.

Who it’s for. Customers ordering ahead, baristas working a live queue during a rush, and managers who need reports, refunds and control over capacity. Brewline is an independent project — not affiliated with any company's app and using none of its code or data.

My role. Solo — proposal, design, engineering, deployment.

The interesting parts

  • Every checkout carries an idempotency key: retries, double taps and dropped connections return the same order.
  • Separate state machines for fulfilment, payment and rewards; staff commands carry an expected version, so two devices can't apply conflicting updates.
  • Order insert, slot-capacity check and reward reservation run in one D1 transaction. Money is integer cents.

The problem

From behind the counter I'd collected the pain points: rewards nobody could explain, checkouts that left people unsure whether they'd been charged, vague pickup times, sold-out items discovered at the counter, and tickets that didn't match what the customer ordered.

Architecture

React PWA  (customer · barista · manager)
        │  /api/*   JSON, cookie session
        ▼
Cloudflare Worker — Hono API
  auth · menu · quotes · checkout · orders · loyalty ledger · refunds · reports
        │                               │
Cloudflare D1 (SQLite)            Stripe Checkout (optional adapter)
  scheduled job every 5 min: expire abandoned checkouts, old rewards, stale orders

Proposal → what's built

  • Rewards used several mental models at once → one model: “$32 of $50 qualifying spend · $18 more to unlock a $5 reward”.
  • Uncertain checkouts → idempotency keys; the app shows “Checking your order…” and never offers a blind “pay again”.
  • Pickup times the café couldn't meet → capacity-aware slots reserved atomically; ASAP honestly says “fully booked”.
  • Sold-out items found at the counter → baristas mark ingredients or items sold out; checkout re-checks availability.
  • Tickets that didn't match the order → structured modifiers validated and priced on the server; substitutions marked with a symbol and bold, not colour alone.

A loyalty ledger that can't double-grant

Rewards are an append-only ledger — EARN, UNLOCK, RESERVE, REDEEM, RELEASE, REVERSE, EXPIRE, ADJUST — and every row has a unique business key, so a duplicate event can never grant a second reward. A reward is reserved at checkout, consumed when the café accepts, released on cancellation; refunds reverse earned progress proportionally.

Payments

Authorize at checkout, capture on acceptance, void on rejection. The Stripe Checkout adapter uses a hosted page, manual capture, signed webhooks with replay protection, and return-page reconciliation if a webhook is lost. A clearly labelled demo card runs alongside it; card data never touches the app.

Security and accessibility

  • PBKDF2 password hashing; hashed session tokens in HttpOnly cookies; separate customer and staff sessions.
  • Role checks on every staff endpoint, object-level ownership checks, rate-limited login and signup, JSON-only mutations as a CSRF defence, CSP and HSTS.
  • Labelled controls, live regions for status changes, status that doesn't rely on colour, 44 px touch targets, reduced-motion support.
Brewline customer app on a phone: café picker, start an order, and rewards progress.

Built & next

Built and working

  • Customer PWA: café picker, menu with sold-out states, customization with live price, bag with server quote, live tracking, receipts, rewards
  • Barista display: queue columns refreshed every 3 s, accept/start/ready, pause, lead time, capacity and sold-out controls
  • Manager console: reports, order search with event timeline, idempotent partial or full refunds, menu and price editing, audit log

Not built yet

  • Email for receipts and password reset
  • Push notifications when the app is closed
  • Tipping, split tender and payroll deduction — these need finance and policy decisions first