khanhanh@sydney: ~/projects/goat-food-and-tea · cat goat-food-and-tea.md
$ cd .. back to ~/projects (esc)

case-study --deep-read

Goat Food In Seattle

Full-stack mobile ordering, payment, and kitchen workflow platform for a Seattle Vietnamese food business.

project: Goat Food In Seattlerole: full-stack product engineerstack: Next.js · Payload · Drizzle · Square · Better Authtimeline: 2026status: live

GOAT Food & Tea is a mobile-first ordering and operations system for a Vietnamese food and drink business Seattle, USA . The product replaces Facebook Messenger ordering with a real flow: browse the menu, customize items, pay online, receive confirmation, and let the owner run preparation and fulfillment from a dashboard.

Problem

The business is small, but the domain is not. Drinks can have bases, sizes, sugar levels, ice levels, included toppings, paid toppings, swaps, party sizes and bundle discounts. The owner also sells desserts and street food, publishes pickup and delivery availability by week, pauses ordering when needed, manages promotions and needs visibility into refunds and collected money.

The main product risk was duplicated truth. If menu prices live in one place, cart totals in another, kitchen preparation names in another and Square charges somewhere else, the business eventually has to reconcile mistakes manually.

Architecture

The app deliberately splits content/configuration from transactional state. Payload owns owner-editable business data: menu collections, pricing, suburbs, open weeks, store settings, front-page content and CMS media. Drizzle owns the transactional order spine in a separate Postgres schema. Better Auth owns the customer identity. Square owns payment and refund execution. Resend owns customer email delivery.

Orders are not Payload documents. They live in an app.orders table with indexes for customer history and kitchen queues. Each order snapshots contact details, fulfillment details, money fields, Square IDs and the exact structured inputs that produced every line item. That snapshot protects order history from future menu edits.

Key decisions

The biggest decision was server-authoritative pricing. The browser sends structured selections, not trusted prices. The server reloads the authoritative menu and pricing, reprices each line, recalculates discount, delivery fee, tax and total, then charges Square. If a selection is stale or unavailable, the order fails before charging.

The second decision was charge-before-insert with compensation. A live order row only exists after the card clears, so there are no unpaid orders in the kitchen. If Square succeeds but the database insert fails, the code immediately requests a refund. The system also checks for an existing order by Square payment ID so a retry cannot create duplicate orders.

The third decision was to snapshot kitchen data in Vietnamese. The customer may browse in English or Vietnamese, but preparation needs stable names for the owner. Kitchen snapshots are built from the Vietnamese catalog at order time so the prep screen remains consistent even if customer-facing copy changes later.

Hard parts

Pricing was the hardest domain. Signature drinks have preset floors, base and size behavior, topping swaps and added topping discounts. Build-your-own uses base-by-size pricing. Party-size items have their own shape. I kept pricing in a shared module so menu display, cart behavior and server repricing use the same rules instead of drifting.

Fulfillment was the next hard part. The owner publishes explicit open weeks, suburbs and slots. There is no automatic rolling schedule. Checkout re-reads live store state at the final pre-charge boundary because availability can change while the customer is deciding.

Refunds added another distributed-system problem. A refund touches PostgreSQL, Square and email; those cannot be made atomic in one transaction. The dashboard persists refund requests with idempotency keys, Square webhooks reconcile final status, and email notification rows use dedupe keys so customer messages remain auditable and retryable.

Owner operations

The dashboard is not an admin skin over a table. It has operations, kitchen, orders and finance views. Kitchen work is grouped by fulfillment date, then by pickup or delivery grouping, with preparation summaries for drinks, sizes, bases and toppings. Finance reporting distinguishes gross, refunds, Square fees, receipt totals and missing-fee states instead of pretending every payment is already reconciled.

What this demonstrates

GOAT shows my strongest full-stack work: domain modeling, transactional data, money handling, server validation, bilingual UX, operational dashboards, external payment reconciliation and testable business rules. It is the clearest example of how I think through systems where correctness matters more than a pretty happy path.

-- EOF · written by khanhanh, edited by nobody● open live ↗next: lapse.md →