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

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
