Live — App Store

RecipeFix

Takes a recipe you found online and rewrites it so you can actually eat it, gluten free, dairy free, vegan or otherwise, without ruining the dish.

Adapts any recipe to a dietary constraint without wrecking the dish — and exposes the whole engine as an MCP server.

Platform
iOS, Web (PWA)
Status
Live — App Store
Demonstrates
MCP server in production
Launched
April 2026

02

What it is for

For people who cook from internet recipes and cannot eat them as written. Celiac and gluten-sensitive cooks, vegans, the dairy-free, parents managing a child's nut allergy, and anyone on keto, paleo, Whole30 or low-FODMAP.

The web is full of recipes and almost none of them are addressed to those people. The usual options are to search for a dedicated version of the dish, which returns something worse, or to guess at a substitution and wreck the texture. This does the substitution properly instead: at a ratio that works, with the quantities rescaled and every step rewritten so the swap survives contact with the method.

03

Why it exists

Every recipe on the internet is written for one diet: the author's.

Swapping an ingredient is not a lookup. Coconut flour absorbs roughly four times the liquid wheat flour does, so "use coconut flour instead" without changing anything else produces a different and worse dish. The people with the problem are anyone cooking around a constraint — a medical one, a religious one, or a household member's — and what they did before was guess, or scroll past.

04

Screens

RecipeFix running on iOS, showing the recipe converter with dietary filter chips and a native tab bar.
01the iOS app: a native tab bar over a web core, which is the whole architectural bet
An adapted vegan recipe showing ingredient swaps and the reasoning behind each one.
02one adaptation, every swap carrying the reason it was made
RecipeFix's Discover screen on iOS with recipe suggestions.
03for anyone who arrives without a URL and no particular plan
RecipeFix's ingredient substitution guides on iOS.
04the substitution rulebook, published — the same rules the adapter runs on

05

What it does

  • Recipe adaptation

    URL, pasted text, or a photo, adapted to fourteen dietary constraints with the swaps and reasoning shown.

  • Diet compliance gate

    A deterministic guardrail on presence-rule diets that blocks a violation and triggers one regeneration.

  • Culinary integrity pass

    Warns on orphan ingredients and substitutions the steps never acknowledge — the failures that ruin a dish.

  • Deterministic scaling

    Serving arithmetic done in code, never by a model, including the rule that you cannot use one and a half eggs.

  • MCP server

    Thirty-five tools over OAuth, each scoped by the caller's own row-level security.

  • Cook Mode

    Step-by-step cooking with timers, on a screen designed to be read with wet hands.

  • Substitution guides

    Fifty-one published guides carrying the same ratios the adapter applies.

  • Browser extension

    Chrome and Firefox, so a recipe can be adapted from the page it lives on.

06

What it runs on

Client

01
React
02
Vite
03
shadcn/ui
04
Tailwind
05
Swift — WKWebView shell

Backend

01
Supabase Postgres
02
56 Deno edge functions
03
Row-level security
04
122 migrations

Services

01
Gemini gateway
02
Anthropic
03
OpenAI (fallback)
04
Stripe
05
Apple IAP
06
PostHog

07

How it's built

A Vite and React front end prerendered at build time, served alongside 56 Supabase edge functions and 122 migrations. The iOS app is a WKWebView shell with a native tab bar, native in-app purchase, Apple Sign-In, a widget and a share extension, generated from a project manifest by XcodeGen so the Xcode project is reproducible rather than committed drift.

The engineering lives in the shared layer rather than in the functions. Three providers — a Gemini gateway, Anthropic and OpenAI as fallback — sit behind a circuit breaker that classifies upstream failures as permanent or transient and skips a dead provider rather than retrying into it. Requests are cached on a SHA-256 hash of a recursively canonicalised request object. Two payment rails, Stripe on the web and Apple IAP in the app, converge on one entitlement flag.

The MCP server is the distribution surface: 35 tools over HTTP with OAuth, hosted as an edge function, where every tool calls Postgres as the signed-in user so row-level security is the authorisation boundary and the MCP path has no privileged access.

08

Architecture

One figure, read left to right. Border treatment carries the node type; the color is the project's.

same engineprovider healthdeterministic gateone regenerationwarningsURL, text, photoextract-recipefunctions/mcp35 tools, OAuth, RLS as calleradapt-recipeGemini, Claude, GPTprovider-breaker.tsSkip permanent, fall backdiet-compliance.tsBlocks, presence-rules onlyserving-scaler.tsArithmetic, no model callculinary-integrity.tsWarns, never blocksRecipeOutputCardSwaps and reasoning shownsaved_recipesRow-level security per user
Fig. 1adaptation, gated twice before you see it
  • Service
  • Store
  • External
  • Model

09

A piece of the code

supabase/functions/adapt-recipe/index.ts

/** Deep-sort keys. NB: JSON.stringify(obj, keysArray) FILTERS nested objects *  to those keys - ingredientScale serialized as {} so "2 lb chicken" and *  "9 cups flour" produced IDENTICAL cache keys (wrong recipes served). */function canonicalize(v: unknown): unknown {  if (Array.isArray(v)) return v.map(canonicalize);  if (v && typeof v === "object") {    const out: Record<string, unknown> = {};    for (const k of Object.keys(v as Record<string, unknown>).sort()) {      out[k] = canonicalize((v as Record<string, unknown>)[k]);    }    return out;  }  return v;}async function buildCacheKey(obj: Record<string, unknown>): Promise<string> {  return sha256Hex(JSON.stringify(canonicalize(obj)));}
Fourteen lines of code and a three-line comment worth more than the code. The second argument to JSON.stringify does not do what it looks like it does, and the result was users being served someone else's recipe. When a standard-library call has a surprising overload, replace it rather than document around it, and leave the failure in the comment so nobody reintroduces it.

Excerpt — trimmed for reading, not a full file.

10

The hard parts

01Problem

Someone selects gluten-free and cooks what comes back. If the model ships wheat flour that is a medical event, not a bad recipe. The obvious fix is to check every diet with a rulebook, and the obvious fix is worse than nothing.

Approach

A diet is checked only where a violation is a property of the ingredient itself, independent of quantity. Presence-rules — vegan, vegetarian, dairy-free, gluten-free, nut-free, halal — are checked and block. Threshold-rules — keto, low-carb, low-sodium — are deliberately not checked, because a correct check needs per-ingredient nutrition keyed to quantity. Low-FODMAP is checked only for ingredients that are high-FODMAP at any normal portion, because the serving thresholds are licensed data the product does not hold.

Tradeoff

Anyone on keto gets no deterministic guarantee, and the product does not pretend otherwise. A regex there would be security theatre: it could not tell five grams of sugar from fifty, and shipping it would imply a guarantee that cannot be made.

02Problem

Diet compliance answers whether it is safe to eat. An integrity pass answers whether it is internally consistent. Neither answers the question the product is actually judged on: if you cook this, does it come out right?

Approach

Two checks that are decidable without judging taste. Orphan ingredients — listed but never used in any step, a pure defect. And uncoupled substitutions: if an ingredient changed and nothing in the steps or notes acknowledges the consequence, the instructions are still describing the original dish and it will fail in the pan.

Tradeoff

These warn rather than block, because unlike a diet violation there is no proof of harm — a recipe can legitimately mention a consequence in wording the check does not match. Blocking on a heuristic would throw away good recipes. The severity of an automated check is matched to the strength of the evidence behind it.

03Problem

Adaptation results are cached on a hash of the request. The original implementation selected which fields to hash with JSON.stringify's second argument, which does not do what it appears to do: it filters nested objects to the same key list.

Approach

The ingredient-scale object serialised as an empty object, so "2 lb chicken" and "9 cups flour" produced identical cache keys and users were served the wrong recipe. It was replaced with a hand-written recursive canonicaliser that deep-sorts keys and preserves nested structure before hashing.

Tradeoff

More code than the one-liner it replaced, and the one-liner looked correct — which is the entire lesson. The failure is recorded in the comment above the function so nobody reintroduces it.

04Problem

When the diet gate fails there are two repair strategies: a targeted patch, or a full regeneration. The patch costs one extra model call if it works and two if it does not, because the failed patch still has to be followed by the regeneration. Which is right depends on how often the patch works, and nobody knew.

Approach

Both were instrumented, and the metrics endpoint reports the number that decides it. Over the last thirty days: three repairs triggered, zero repaired by patch, three fell back to full regeneration.

Tradeoff

The measurement currently says the patch stage is pure overhead on the slowest path in the product. The sample is three. So it stays, instrumented, until the number means something — which is a less satisfying answer than deleting it.

Ask about RecipeFix

Answers are drawn from the notes and source behind this case study, and cite what they read.

11

What it did

132

Adaptations, last 30 days

Vegan

Most requested

2.4% caught

Compliance gate

31

MCP tools

Measured 26 August 2026