Selected work · nutrition ai at the edge
Foodie · nutrition intelligence at Cloudflare's edge
A recommendation engine that pairs travelers with the right dishes at any restaurant in the world. Three LLM providers behind one interface, session state in Durable Objects, and a menu ingestion pipeline that turns tens of thousands of dishes into scored, taste-aware suggestions.
Senior Full-Stack Engineer / Backend Architect
2023 to 2025
foodie · orchestration, sessions, ingestion workflows
StackShow 8 itemsHide
Context
Flykitt's protocols tell travelers when and what to eat. A protocol that says "low inflammation, high protein" doesn't survive contact with an unfamiliar menu in another language, though. Foodie closes that gap. Point it at a restaurant anywhere in the world and it scores the menu, shortlists the dishes that match your goals, and proposes modifications when a good dish is one substitution away from being a great one.
Problem
Menus are unstructured, inconsistent, and endless. The moment of need is standing in front of one, which makes latency part of the product. The engine had to ingest hundreds of restaurants offline, then serve conversational, taste-aware suggestions online, while keeping per-request LLM cost from scaling with menu size.
One menu, hundreds of dishes
A traveler opens a menu with a taste profile in their pocket and about a minute of patience. The engine on the right has already ingested the restaurant, scored every dish, and reduced the choice to a shortlist, before the waiter comes back.

What every unfamiliar menu asks the traveler to decide
Architecture
Every part of Foodie runs on Cloudflare. A Worker fronts the product. Session state lives in a Durable Object per user. Concurrent lookups for the same place converge on a second Durable Object per (geohash, profile) key, so two travelers opening the same menu at the same moment do the recall, the rerank, and the LLM pass exactly once. Spatial recall is real PostGIS behind Hyperdrive, on a GiST-indexed geography column. Vectorize prefilters against the taste profile. LLM traffic egresses through AI Gateway to OpenAI, Anthropic, and Cohere, each routed to the step it is best at. The request path never leaves Cloudflare, and there is no shared connection pool to warm up.
one worker at the front, two kinds of durable object behind it, external llms only via ai gateway.
Restaurant data enters the system three ways: on-demand when a search hits an unknown tile, on a scheduled refresh for stale tiles, and directly when a restaurant signs up. Every path lands in Queues, fans out per restaurant, and runs one Cloudflare Workflow per restaurant with durable steps. Fetch, OCR (Workers AI vision on scanned menus), extract to canonical dish records (LLM via AI Gateway), normalize and translate, geocode into a PostGIS geography column, embed each dish with bge-large on Workers AI, and commit atomically to Postgres, Vectorize, R2, and Cloudflare Images. A failed step resumes on retry; the whole restaurant never restarts.
The read path is a hierarchy that answers without doing work whenever it can. Cache API first, 30 seconds. KV next, 24-hour scoreboards keyed by geohash plus profile. Then the Search Durable Object, one instance per (geohash6, profileHash) key. If two travelers hit the same key at the same time, the second attaches to the first request's in-flight promise instead of rerunning the pipeline. On a miss, the DO runs PostGIS ST_DWithin over the GiST-indexed geography column, filters candidates through Vectorize against the taste profile, and batches LLM scoring through AI Gateway. Results write back to the DO, then KV, then Cache API, so a third caller within the TTL is a straight cache hit.
Menu photos and dish imagery flow through R2 for originals and Cloudflare Images for delivery. Signed variants for the printable scoreboard PDF, thumbnail and hero variants for the app, one origin regardless of whether the image came from a scrape, an upload, or a Workers AI generation.
The product in production
What the engine actually produces, shown as-shipped: four recommendation cards spanning four cuisines, and the two product screens travelers meet first. The UI is the work of the wider Flykitt team; the pipeline that fills these cards is the scope of this case study.
Cross-cuisine recall
One scoring pipeline, four unfamiliar menus.
Same taste profile, four cities. Each card is composed from the same ingest, analyze, and score pipeline. Dish shortlists, macro ratings, and score rings all fall out of the shared engine, not hand-authored per restaurant.




Two doors, one pipeline
How the traveler hands a menu to the engine.

The traveler opens Foodie to one of two doors: scan the menu in front of them, or search healthy restaurants nearby. Past scans keep working sessions warm.

Handing the engine a menu: a place name plus one or more photos. The upload kicks the Workflows pipeline; the session Durable Object holds the traveler's taste profile through the wait.
Key decisions
Session state in Durable Objects, not in a database.
A recommendation session is a conversation: taste profile accumulates, suggestions get rejected, context builds. A Durable Object per session keeps that state at the edge next to the compute, with strong consistency and no connection-pooling to a shared store.
Impact·Personalized sessions survive network hops, restarts, and out-of-order events, with zero session-store operations to run.
More than one LLM provider, behind one interface.
Dish extraction, conversational suggestion, and rerank have different quality, latency, and cost profiles. Routing across OpenAI, Anthropic, and Cohere per task, rather than standardizing on one vendor, lets each step use the model that is actually good at it.
Impact·A provider outage degrades one capability instead of the whole product. Per-task cost becomes a routing decision rather than a rewrite.
Long-running analysis belongs to Workflows, not requests.
Menu ingestion (fetch, parse, extract, score, index) is batch work with retries, not request work. Cloudflare Workflows carries the pipeline with durable progress, so a failed step resumes instead of restarting the whole restaurant.
Impact·Tens of thousands of menu items processed reliably as a background concern. The traveler feels progress, not failure.
One Search Durable Object per (geohash, taste-profile) key.
Two travelers in the same neighborhood with the same dietary profile don't need two of everything. Naming the DO with hash(geohash6, profileHash) puts them on the same instance; the DO holds an in-flight promise so the follower attaches instead of restarting the pipeline.
Impact·Duplicate concurrent lookups collapse to one compute. Followers within the TTL are Cache API hits, so no Postgres, no Vectorize, no LLM tokens.
Postgres + PostGIS behind Hyperdrive, not a homegrown geohash index.
Nearby-restaurant recall wants real spatial SQL (ST_DWithin over a GiST-indexed geography column), not an approximate tile lookup. Hyperdrive keeps the Cloudflare-only story honest: the Worker binds to Postgres like any other resource, no cold connections, no AWS credentials on the request path.
Impact·Recall is a range scan, not a table scan. Adding filters (cuisine, price band, hours) stays inside the same query plan without a schema rewrite.
Outcomes
menu items analyzed into scored, personalized suggestions
reduction in customer decision time in front of a menu
LLM providers orchestrated behind one session model
The pipeline runs in production against real travelers on a paid tier. Beyond the numbers, the choice to isolate the AI workloads at Cloudflare's edge (rather than in Flykitt's AWS core) kept the rest of the platform's failure domain intact through the entire Foodie build.
Provenance of numbers:MmeasuredIimplemented