Joy Curry
Production ordering platform for a Manhattan restaurant
Solo build — architecture, frontend, API, database, and deployment.
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."
Architecture
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().
Technical Decisions
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).
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.
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.
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.
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.
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.
Engineering Challenges
Pricing an order the restaurant can't deliver itself
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.
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.
A menu that never lies
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.
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.
Building for a client that doesn't exist yet
An Expo mobile app was on the roadmap from day one — duplicating menu logic/cart math/API types into a second codebase risks drift.
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.
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.
KISMET
Local-first image-collection CLI — precise queries, zero API keys
