-
-
EU Big 14 allergen picker - every ingredient is classified, not string-matched. Plus custom search across all 1,790 Epicure ingredients.
-
Personalised Discover tab — taste profile, allergen-filtered trends, and pre-computed recipe suggestions from a background Lambda.
-
Allergen-safe recipe with nudge buttons for mid-flight steering and a personalised 'Why is this safe for me?' explainer.
-
The agentic brief — Claude Haiku reasons over Maya's taste history before Claude Sonnet generates. The AI thinks out loud.
-
Safe Foods Mode — Seren's kitchen. Recipes use only these 9 ingredients. The feature no other recipe app has
-
Diet & Lifestyle presets — Vegan, Keto, Low-FODMAP, Lactose Intolerance, No Alcohol, Low Histamine. Each with granular sub-options.
-
Settings — spice tolerance, adventurousness, Safe Foods Mode, macros off by default, and per-section Discover controls.
-
Allergen-safe substitutes ranked by similarity and dish context fit — with plain-English explanations of why each works.
-
Kitchen management — subtype, quantity, storage area, and expiry tracking. 27 ingredients, all searchable across 1,790 Epicure items.
-
Substitute any recipe ingredient — scored by similarity and dish context fit, with 'Use this' applying the swap instantly.
-
Claude Vision scans your fridge — infers storage area, flags uncertain matches, detects duplicates already in your kitchen
-
Seven DynamoDB tables — each with a deliberate data model. fable-users carries the GSI powering the agentic taste profile pipeline.
-
System architecture — 7 DynamoDB tables, 4 least-privilege Lambdas, DynamoDB Streams, EventBridge Scheduler, and API Gateway.
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 tofable-users, incrementsfable-ingredient-insights, setsneedsRecompute = "true"fable-taste-profile-writer: runs on EventBridge every 6 hours, queries theneedsRecomputeGSI instead of scanning the whole table, runs drift analysis, calls Claude Haiku, writes aStoredTasteProfileback tofable-usersfable-vision-ingredient-scanner: API Gateway endpoint, Claude Haiku 4.5 Vision, three-tier Epicure fuzzy matching, returns an ingredient list with confidence flagsfable-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:
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
- amazon-api-gateway
- amazon-cloudwatch
- amazon-dynamodb
- amazon-eventbridge
- anthropic-claude
- aws-iam
- aws-lambda
- better-auth
- claude-haiku
- claude-sonnet
- dynamodb-streams
- epicure
- framer-motion
- jest
- neon
- next-intl
- next.js
- node.js
- open-food-facts
- postgresql
- pwa
- react
- tailwind
- tanstack-query
- typescript
- vercel
- zxing-js
Log in or sign up for Devpost to join the conversation.