We Cooked
A recipe book for a two-person household. Paste a link or a photo, Claude does the typing, you check it before it saves.
2026. Live
- SvelteKit
- TypeScript
- Swift
- SwiftUI
- SQLite
- Claude API
- Fly.io
Problem
We cook from a phone propped against the kettle. Recipes arrive as links, screenshots, pasted text, and photos of cookbook pages, and none of them agree on units, yields, or layout. Typing each one into an app is the step nobody does. I wanted a private recipe book where a recipe arrives in whatever shape it comes in, becomes one clean, scalable, convertible recipe without typing, and never saves anything a person has not looked at.
Approach
One Node process serves both a SvelteKit web app for the laptop and a versioned JSON interface for the native iOS app. Extraction is a job: the input goes to Claude with a request for schema-matching JSON, the draft opens in an editable review form, and nothing is saved until a person confirms it. Claude is called even when a page already has structured recipe data, because the structured data is usually worse than the page.
Two users means no feature is justified by scale, so the whole thing runs on one machine with SQLite as the only database. The job queue is a table: a worker claims a job with one atomic update, and anything still marked running at boot is marked failed. Every Claude call goes through one wrapper that takes a slot from a daily quota of fifty calls, so a bug cannot run up a bill overnight. The URL fetcher refuses loopback, private, link-local, and cloud metadata ranges and re-checks every redirect hop by hand. Sites that block server fetches get a paste path instead of a scraping arms race, and later the phone renders the page itself and the server fetch became the fallback. The machine never overwrites a human edit. Backups go through Litestream to object storage because the host’s volume snapshots can lose data, and the restore was drilled once before launch.
The web app and the iOS app are kept at parity through a matrix of 104 rows that is updated with every change.
Tech
SvelteKit and Svelte 5 on Node 22, TypeScript, better-sqlite3, the Anthropic SDK, Argon2id for the shared password, sharp for images. The iOS app is Swift and SwiftUI on iOS 17 with a shared package for models, the API client, the keychain, and a job poller, plus a share extension and a widget. One Fly.io machine with a 3 gigabyte volume, Litestream replicating to Cloudflare R2, which also holds photos behind presigned links. GitHub Actions runs type checks and tests on every push. 235 server tests, 182 Swift tests, and 46 decision records.
My contribution
The product spec and all 46 decision records are mine, and so are the choices that define the system: one process and one database, review before save, the daily spending cap, the fetch guard, the never-overwrite rule, mandatory Litestream, and the parity matrix. I chose the stack against an earlier recommendation of htmx and Python that turned out to be wrong for this app. The iOS port went out in nine units to TestFlight over three days. I review every change on a real phone at a real stove.
AI usage
104 of the 120 commits carry a co-author trailer for the model that wrote the code. The server tests write JSON fixtures that the Swift tests decode, which is how I keep an agent working in one language from drifting away from an agent working in the other. The iOS design was chosen from three candidate designs judged by a different model. A timestamped decision trail records each point where an agent asked and I chose.
Lessons
An untested backup is a belief, not a backup. I retired the web app in September, and within three days the gaps that caused showed me it was the wrong call, so it came back at parity. The paste path I added as an escape hatch turned out to be the main path, because the biggest recipe publishers block server fetches, and the right fix was to let the phone fetch the page. And the cost of the model’s reasoning tokens was the least certain number in the whole spec, which is why the daily cap exists.