Case Study · 01

Joy Curry

Production ordering platform for a Manhattan restaurant

Solo build — architecture, frontend, API, database, and deployment.

Astro SSRReact 19TypeScriptExpressPostgreSQLJWT/bcryptTurborepopnpm
145
Menu items
12
Categories
10
SQL migrations
3+1
Workspaces · monorepo
01

Overview

Joy Curry & Tandoor is a Manhattan restaurant (est. 1994). Joy Curry is the production ordering platform built for it end to end — a customer storefront with a 145-item menu, cart and combo builder, and checkout, plus a role-gated admin panel for running the catalog day to day. Live on Vercel and Render.

It's a pnpm + Turborepo monorepo where an Astro SSR frontend (React only where it needs to hydrate), an Express + PostgreSQL API, and a shared @joy-curry/core package are separate workspaces — so cart math, formatters, and the API contract are written once and can later power an Expo mobile app without a rewrite. The README frames it as "share logic, not pixels."

01
Storefront home with hero and category browser
02
Menu grid showing items with photos and prices
03
Cart and combo builder with size and modifier options
04
Admin panel with stock and price controls
02

Architecture

Browser → Astro SSR frontend (Vercel) → HTTPS → Express API (Render, REST) → JSON/JWT → PostgreSQL

Out-of-zone orders fan out to courier partners (Uber/DoorDash), with a deterministic simulated quote as fallback so checkout never dead-ends. Middleware: verifyToken(), requireRole().

BrowserClient
Astro SSRVercel
Express APIRender · REST
PostgreSQLOrders · Menu · Users
Courier fan-out (Uber / DoorDash) + sim fallback
03

Technical Decisions

Rendering

Astro SSR + React islands, not a full SPA

Menu/marketing pages render as fast SEO-friendly HTML server-side; client JS ships only where interaction lives (cart, checkout, admin toggles).

Money

Integer cents everywhere

Every price is an integer (price_cents: 1550 = $15.50), no floats, formatted only at render — prevents rounding drift across 145 items + combos.

Persistence

Hand-written SQL migrations over an ORM

Real relational constraints (CHECKs, foreign keys, UNIQUE idempotency_key). Ten sequential migrations keep schema changes explicit and reviewable.

Menu truth

is_active split from in_stock

"Retired from menu" vs. "86'd for today" are different facts — sold-out dishes show as sold out instead of vanishing.

Auth

Stateless JWT + bcrypt

Frontend (Vercel) and API (Render) live on different origins, so server sessions were out. Signed tokens + bcrypt-hashed credentials + requireRole() gate admin.

Resilience

Degrade gracefully with no keys

Geocoding/courier dispatch fall back to safe defaults when credentials aren't set — fully testable locally with zero third-party keys, flips to live the moment keys are added.

04

Engineering Challenges

Pricing an order the restaurant can't deliver itself

Problem

In-house delivery has a fixed radius; out-of-zone orders need Uber Direct/DoorDash Drive, which are external, slow, sometimes down — and the platform had to work locally with zero courier credentials.

Solution

deliveryPartners/index.js queries every configured partner in parallel via Promise.allSettled, drops rejected quotes, takes the cheapest fulfilled one. Falls back to a deterministic simulated quote ($5.99 + $1.99/mile). A failed live dispatch still returns a sim_* id so the order completes and gets flagged for manual handling.

apps/api/services/deliveryPartners/index.js

A menu that never lies

Problem

Kitchens change hourly — dishes sell out, get retired, come back seasonally. One flag collapses "sold out" into "gone," and deleting retired items breaks historical order references.

Solution

Availability modeled as two independent facts — is_active and in_stock — behind separate API routes; items are soft-deleted so past orders keep resolving to real records.

migrations/ · admin menu routes

Building for a client that doesn't exist yet

Problem

An Expo mobile app was on the roadmap from day one — duplicating menu logic/cart math/API types into a second codebase risks drift.

Solution

Platform-agnostic domain types, cart/formatting logic, and the API client live in @joy-curry/core, consumed by the web app today; a future client starts from the same core instead of a rewrite.

packages/core
05

Outcome

Live and taking real orders — 145 items across 12 categories, in-house and courier delivery, admin panel the restaurant runs itself. Integer-cents math means zero rounding error; the entire checkout flow runs locally with no third-party keys. The shared core already holds types/business logic, so the planned Expo mobile app is additive, not a second build.

Next case study

KISMET

Local-first image-collection CLI — precise queries, zero API keys