The Recipe Room — recipe management system
Why I built it. I love cooking, and I collect recipe ideas everywhere: web pages, social media, cookbook photos. My "system" was bookmarks and screenshots, which meant scrolling through hundreds of images every time I wanted to cook something again. The Recipe Room replaces that with one searchable place: photograph any recipe and AI extracts it into structured, editable data. It's an ongoing project, and it doubles as my proving ground for owning a system end to end: schema, API, auth, AI pipeline, and UI.
case.architecture — the whole system
System architecture
Three trust zones: the browser, my server code, and third-party services. The client never talks to OpenAI directly and never holds a privileged database key. AI calls, image uploads, and search are brokered by API routes on the server. Recipe reads and writes go from the browser to Supabase directly with the public anon key, and Postgres Row Level Security does the authorization: write policies scope rows to the signed-in user, and a few read policies expose featured recipes publicly.
case.flow — the interesting path
Request flow: photo in, recipe out
The signature feature is turning a photo of any recipe, a cookbook page or a handwritten card, into structured data. Here's the full round trip:
- Upload. The user photographs a recipe. The client compresses and posts it to
/api/analyze-recipe, never to OpenAI directly, so the API key stays server-side. - Extract. The route sends the image to gpt-4o with a prompt that demands a strict JSON shape: title, ingredients, instructions, prep and cook time, servings, category.
- Validate. The response is parsed and validated against the
Recipetype before it ever reaches the client. Malformed AI output fails gracefully with an actionable error, not a broken form. - Review. The extracted recipe pre-fills the editing form. AI output is a draft, not a commit. The user confirms and corrects before anything is saved.
- Persist. On save, the recipe is written to Postgres straight from the browser with supabase-js. The Row Level Security policy checks ownership in the database, so client code cannot write into someone else's collection.
case.decisions — tradeoffs I made on purpose
Architecture decisions
Authorization lives in the database
Recipe reads and writes go straight from the browser with supabase-js, so Row Level Security is the authorization layer. Write policies scope rows to the signed-in user, and a few read policies deliberately expose featured recipes to everyone. Middleware only guards protected pages. The tradeoff is writing and testing SQL policies alongside app code, but a policy in Postgres cannot be bypassed by a bug in my UI, and it removed an entire tier of CRUD endpoints I would otherwise have to build and maintain.
Server state is a cache, not a store
Recipes are server-owned data, so they live in React Query, with the cache persisted to storage so the app opens with recipes instead of spinners. Mutations write through on success and invalidate their queries, which keeps the cache honest without optimistic bookkeeping. Redux Toolkit handles what is genuinely client state: the search controls and the chef widget. Two tools, each doing the job it was built for.
AI behind an API, treated as untrusted input
Three routes, photo analysis, URL extraction, and the chef chat, all call gpt-4o server-side, so the API key never reaches the browser. Model output is validated like any user input and lands in a review step before persisting: useful when it works, harmless when it hallucinates. And because the client only knows the routes, swapping the model never touches the UI.
case.frontend — where the ux engineering shows
Frontend architecture
The UI is a vintage cookbook: warm cream, classic recipe cards, and handwritten-feeling type, built on a token layer in design-system/tokens so the theme is data, not scattered CSS. I also put focused work into how fast it feels: skeleton loaders match the final layout so nothing shifts, Lottie animations are preloaded, images run through Next.js optimization, and routes code-split on their own. Offline, the service worker keeps the app usable in the kitchen, which is where a recipe app actually gets used, on a flour-covered phone with one hand.
case.transfer — what this work makes me good at
What transfers
I can take a product from database schema to shipped UI on my own. I give model output the same scrutiny as user input. Everything the AI returns is validated and reviewed before it's saved. I put the security checks in the database, where they still hold if my application code has a bug. And I pay close attention to how fast an app feels, which is a different problem from how fast it benchmarks.