Inspiration

Most recipe apps are built for people who can eat anything.

250 million people worldwide live with food allergies. Millions more live with MCAS, Mast Cell Activation Syndrome, a condition where the immune system reacts unpredictably to food triggers that vary from person to person, and one that's probably underdiagnosed. For people in Safe Foods Mode, eating only a list of ingredients they know won't cause a reaction, every existing recipe app is useless. Not unhelpful. Useless.

Three years ago my MSc dissertation built an ingredient embedding system using Neo4j, GraphSAGE, and GPT-3.5 to generate novel recipes. Part of that research identified the western bias in food datasets and flagged allergen-aware generation as the obvious next step. I flagged it, and then life happened, and I moved on.

Then, on 26 May 2026, three weeks before this submission, Epicure was published on arXiv by Radzikowski and Chen at KAIKAKU.AI. The largest multilingual food embedding model ever trained: 4.14 million recipes, 7 languages, 1,790 ingredients compressed into 2MB of vector space. They'd solved the dataset problem I'd been thinking about for three years. They just hadn't built anything on top of it.

Hack the Zero Stack gave me the reason to finally build it. Fable is that application layer.


What it does

Fable is an allergen-aware recipe discovery and generation app. You tell it what you can't eat. It tells you what you can cook.

Every one of Epicure's 1,790 ingredients is explicitly classified against the EU Big 14 allergens. O(1) lookup, no string matching, no false positives. Oat milk is safe for dairy allergies but contains gluten, so it's unsafe for coeliac users. Almond milk is safe for dairy allergies but not for tree nut allergies. Apple cider is alcoholic in the UK. Fable knows all of this because it was taught it, not because it guessed.

Safe Foods Mode. This is the feature no other recipe app has. For MCAS and severe allergy users who can only eat a specific list of ingredients, recipe generation is strictly constrained to that list. "Liquid of choice" and "seasoning of choice" placeholders exist because for some users, even water isn't a safe assumption.

"Why is this safe for me?" A Claude Haiku call reads your specific allergen profile, diet presets, and Safe Foods Mode, and explains in plain English exactly why this recipe is safe for you. Not a generic disclaimer. An explanation that's actually about you. For someone with a severe allergy, that's the difference between trusting the app and not.

Agentic recipe generation. A two-step flow where Claude Haiku reasons over your taste history and writes a recipe brief before Claude Sonnet generates the recipe. You can see it thinking. You can steer it mid-flight with nudge buttons, make it spicier, go vegetarian, try a different cuisine, and AbortController quietly cancels the in-flight request while the brief card updates. It never feels like a restart.

Role-aware substitution. When a recipe calls for something you can't have, Fable finds the nearest safe substitute using Epicure's embedding geometry. It understands what the ingredient is actually doing in the dish, whether that's fat, binding, or acidity, and finds something that does the same job. Pasta can't substitute for cheese in a pasta bake, even though they show up together constantly in the training data.

Personalised taste profile. Every like and dislike feeds a preference model. A background process on EventBridge Scheduler picks up on drift, what's emerging in your taste and what's fading. Flavour territory comes from the geometric overlap of your top five preferred ingredients' embedding neighbourhoods. Recipe suggestions are pre-computed and waiting for you in the Discover tab before you've asked for anything.

Diet and lifestyle presets. Vegan, Vegetarian, Keto, Low-FODMAP, Lactose Intolerance (two sub-modes), No Alcohol (UK-aware, two sub-modes), Low Histamine (85+ Epicure-verified keys, with a medical disclaimer).

Spice tolerance and culinary adventurousness. Two preferences set during onboarding that quietly shape recipe generation, the agentic brief, and substitution scoring all at once. Someone cautious gets safe, comforting recipes. Someone adventurous gets nudged toward flavour territory they haven't tried yet.

7 languages. Fable ships in all 7 languages Epicure was trained on: English, Spanish, French, German, Italian, Simplified Chinese, Japanese. Browser locale is auto-detected. Adding a new language is just one JSON file.

PWA. Installable on any device, works offline for the app shell.


How we built it

The AWS architecture is the spine of the product, not something bolted on afterward.

Seven DynamoDB tables, each with a deliberate data model and access pattern:

Table Purpose
fable-users Allergen profiles, kitchen contents, preference signals, taste profiles, spice tolerance, adventurousness, alcohol mode, low histamine flag
fable-feedback Like/dislike data with DynamoDB Streams enabled. This is the entry point for the entire personalisation pipeline
fable-ingredient-insights Aggregate trending data per allergen profile and time window
fable-rate-limits Atomic dual-window (hour + day) counters via TransactWriteItems. Fail-open. TTL auto-cleanup
fable-saved-recipes Full recipe objects. TTL on unsaved history (90 days), no TTL on saved recipes
fable-recipe-shares Public share records. 90-day TTL. Anonymous, no userId stored
fable-collections Named recipe groups

fable-users also carries a GSI, needsRecompute-lastComputedAt-index, which the taste profile Lambda queries directly instead of scanning the whole table.

Four AWS Lambda functions, all least-privilege IAM:

  • fable-feedback-stream-processor: triggered by the DynamoDB Stream, extracts preference signals, writes to fable-users, increments fable-ingredient-insights, sets needsRecompute = "true"
  • fable-taste-profile-writer: runs on EventBridge every 6 hours, queries the needsRecompute GSI instead of scanning the whole table, runs drift analysis, calls Claude Haiku, writes a StoredTasteProfile back to fable-users
  • fable-vision-ingredient-scanner: API Gateway endpoint, Claude Haiku 4.5 Vision, three-tier Epicure fuzzy matching, returns an ingredient list with confidence flags
  • fable-barcode-scanner: API Gateway endpoint, calls Open Food Facts with a 5s timeout, same three-tier Epicure matching. Zero npm dependencies. Barcode values never go anywhere near Claude

The personalisation loop, end to end:

feedback → DynamoDB Stream → Lambda → preference signals
→ GSI flag → EventBridge → drift analysis → Claude Haiku
→ stored taste profile → Discover tab

The whole pipeline runs independently of whatever the user is doing. By the time you open the app, the work's already been done.

The monetisation boundary is already enforced at the infrastructure level. Guests cost nothing to serve. Authenticated users are rate-limited by atomic DynamoDB counters. The free/paid split isn't something on a roadmap somewhere, it's how the app actually works right now.

Architecture diagram:

Fable System Architecture

The stack: Next.js 16, React 19, TanStack Query v5, Framer Motion, Better Auth 1.2.7 with Neon Postgres for auth, next-intl for i18n. Deployed on Vercel. 867 passing tests across 47 suites.


Challenges we ran into

next-intl 404'd the entire app on Vercel.

It worked fine in development. The moment it went live on Vercel, every single page on the site returned a 404.

The cause turned out to be a default behaviour buried in next-intl: it tries to redirect every page to a language-specific URL like /en/, even when you've explicitly told it not to. Fable doesn't use that kind of URL structure, so every one of those redirects pointed somewhere that didn't exist, and Next.js had nothing to serve.

The fix was to stop letting next-intl handle routing at all, and instead detect the user's language directly from a cookie, falling back to their browser's language setting if there isn't one. Same result for the user, none of the redirect problem.

What I took from it: the behaviour was working exactly as documented, it just wasn't obvious from reading the docs alone what that would actually do to a real deployment. Reading the actual source code earlier would have caught it sooner.


A substitution threshold that didn't hold up.

The substitution engine originally penalised any candidate whose average cosine similarity to the other ingredients in the dish went above 0.7, on the theory that it was probably a co-ingredient rather than a real substitute.

The problem was that 0.7 is just a number I picked. It wasn't grounded in anything about Epicure's actual embedding space. A dense five-ingredient context could push a perfectly good substitute above 0.7 by accident. There was also an ugly cliff at the boundary: 0.699 got a bonus, 0.701 got a penalty, a 0.41 point swing for nothing.

I rewrote it as a relative penalty instead. Rather than asking "is contextFit above 0.7?", it asks "is this candidate more similar to the dish around it than to the thing it's replacing?" If averageContextFit > similarityToOriginal + 0.15, it gets penalised. The threshold adjusts itself to wherever Epicure's embedding space actually sits.

The lesson here: if you're picking a threshold in an embedding space and you can't actually justify the number, that's usually a sign it should be relative instead of fixed.


Accomplishments that we're proud of

I caught a bug partway through where kitchen ingredients weren't being filtered against dietary restrictions before going to Claude, which meant the model could receive contradictory instructions about the same ingredient. In an app built around keeping people safe, that's a safety fix, not a polish fix.

The agentic architecture. EventBridge, DynamoDB Streams, Lambda, and a GSI working together as a genuinely event-driven system. The taste profile writer runs on its own schedule, completely separate from anything the user is doing, and the GSI means it only ever touches users who actually have new feedback. The suggestions sitting in the Discover tab are the output of a background process that ran hours earlier, not something computed on the spot.

Safe Foods Mode. As far as I know, this is the only consumer recipe tool that constrains generation to a user-defined safe ingredient list, with server-side validation catching anything the model tries to add outside it. "Liquid of choice" and "seasoning of choice" placeholders exist because for some users, even water isn't something you can assume is safe.

The infrastructure already enforces the free/paid boundary — guests cost nothing to serve, authenticated users hit atomic DynamoDB rate limits, and the access control layer is already live. Stripe is genuinely the last step.

867 passing tests across 47 suites, covering allergen safety and filter accuracy specifically. The low histamine ingredient list was cross-checked against the full Epicure vocabulary before it went in. The substitution scoring formula was tested against known-good Epicure cosine similarities, not just spot-checked by eye.


What we learned

Designing the data model properly upfront paid off. The GSI on fable-users is there because I knew a full table scan in the taste profile Lambda would become a real problem once there's more than a handful of users, so it was worth the extra thought even at hackathon speed.

The Low Histamine bug taught me that giving an AI model two contradictory instructions in the same prompt isn't just a quality problem, it's a safety problem if the thing you're building is meant to keep people safe. Validating on the frontend makes the app nicer to use. Validating on the backend is what actually stops something unsafe from reaching the user, and you need both, not one instead of the other.

If I can't explain why a threshold is the number it is, that's usually a sign I picked it rather than derived it. The substitution scoring taught me that the honest fix is often to make the threshold relative to the data instead of pinning it to a number that felt right at the time.

And the free/paid boundary belongs in the infrastructure, not the interface. A button that's disabled in the UI can be worked around. A Lambda that simply doesn't have permission to do something can't.

Not every fix was a big lesson. better-auth/react turned out to crash Next.js's static prerendering because of something it does at startup, and the fix was simply not depending on it, a hand-rolled client talking to the same API endpoints directly. Small problem, small fix, but worth a line.


What's next for Fable

Right after the hackathon:

  • Move auth off Neon Postgres and onto AWS RDS Postgres. Schema's identical, it's basically a connection string swap, and it completes the AWS story properly
  • Add social auth (Google and GitHub). Better Auth's already got the placeholders, Neon's schema is ready
  • Stripe. The rate limiting and access control already enforce the free/paid boundary, charging for it is genuinely the last step

Further out:

  • Multilingual recipe output. The UI ships in 7 languages now, but generating recipes in the user's own language is a separate piece of work for after the hackathon
  • A native mobile app, which would unlock camera and barcode features properly without PWA limitations
  • Quantity recognition in photo scanning, so it can tell you it sees 3 eggs, not just "eggs"
  • Epicure Chem integration for cross-reactivity research, like detecting oral allergy syndrome (birch pollen reacting with apple or carrot). This one needs a clinical partner before it goes anywhere near real users

The business model is usage-based freemium, and the infrastructure for it is already built. What's actually worth thinking carefully about is the price point, because for the people this app is for, a bad recipe suggestion isn't a minor inconvenience. It's a health risk. That changes the conversation about pricing in a way most apps don't have to deal with.

Built With

Share this project:

Updates

posted an update

Day 1-2: From idea to full-stack app in 48 hours

When I saw the H0 Hackathon announced, I knew immediately what I wanted to build. As someone who researched AI-driven food intelligence for my MSc dissertation — building ingredient embeddings, graph neural networks, and novel recipe generation — I've always believed food AI could do more than just recommend popular recipes. It should work for everyone, including the 250 million people worldwide living with food allergies.

Fable is an allergen-aware recipe discovery app powered by Epicure — the largest multilingual food embedding model ever built (4.1M recipes, 7 languages, 1,790 ingredients compressed into 2MB of vector space).


Allergen picker

Allergen picker

EU Big 14 allergen filtering with a hand-curated truth table across all 1,790 ingredients. Edge cases handled correctly — oat milk is safe for dairy allergy, almond milk is not safe for tree nut allergy.


AI recipe generation

Recipe generation

Anthropic Claude generates novel restaurant-quality recipes based on your ingredients, allergen profile, meal type, and cook time. Every recipe is unique — powered by Epicure's flavour embeddings finding what works together.


Safe Foods Mode

Safe Foods Mode

Built specifically for MCAS and severely restricted diets. Define exactly what you can eat and Fable generates recipes strictly within that list. "Liquid of choice" placeholders for users who can't even assume water is safe. Nobody else is building recipe tools for this community.


Kitchen management

Kitchen management

Track ingredients by area (fridge, freezer, cupboard, pantry), set use-by or bought dates with automatic shelf life calculation, add quantities and subtypes. "Use my kitchen only" toggle constrains recipes strictly to what you have.


Tech stack

Next.js 16 · TypeScript · Tailwind CSS · AWS DynamoDB · Anthropic Claude · Epicure Core embeddings · Vercel

Try it

Live app · GitHub

Log in or sign up for Devpost to join the conversation.

posted an update

Day 1-2: From idea to full-stack app When I saw the H0 Hackathon announced, I knew immediately what I wanted to build. As someone who researched AI-driven food intelligence for my MSc dissertation — building ingredient embeddings, graph neural networks, and novel recipe generation — I've always believed food AI could do more than just recommend popular recipes. It should work for everyone, including the 250 million people worldwide living with food allergies. Fable is an allergen-aware recipe discovery app powered by Epicure — the largest multilingual food embedding model ever built (4.1M recipes, 7 languages, 1,790 ingredients compressed into 2MB of vector space). What's working in 48 hours:

EU Big 14 allergen filtering with a hand-curated truth table for all 1,790 ingredients — catching edge cases like oat milk (safe for dairy allergy) and almond milk (unsafe for tree nut allergy) Three recipe modes: ingredient pairings, AI recipe generation, and Safe Foods Mode Safe Foods Mode — built specifically for MCAS and severely restricted diets. Users define exactly what they can eat and Fable generates recipes strictly within that list, with "liquid of choice" placeholders for users who can't even assume water is safe Kitchen management with area tracking (fridge/freezer/cupboard/pantry), use-by dates, bought dates with automatic shelf life calculation, quantities and subtypes Full AWS DynamoDB persistence — allergen profiles, saved recipes, kitchen contents all survive sessions Deployed live on Vercel

Tech stack: Next.js, TypeScript, Tailwind, AWS DynamoDB, Epicure Core embeddings, Anthropic Claude claude-sonnet-4-6 Live app: https://v0-allergen-recipe-app.vercel.app More features incoming this week.

Log in or sign up for Devpost to join the conversation.