A recipe page that began as a single self-saving AI artifact and grew in three weeks into a hardened, multi-user AWS app with Google sign-in, photo storage, and over 4,000 tests.
Cookbook stores recipes with photos, filters them by ingredient, and builds a shopping list that merges ingredients across recipes. It is phone-first (bottom nav, sheets, installable as a PWA) and keeps a full desktop layout.
It started life as one HTML artifact on claude.ai that saved itself. Splitting that file into a real repo kicked off a sprint to AWS serverless: S3 and CloudFront in front, three Lambdas behind API Gateway, DynamoDB underneath, all defined in CloudFormation and deployed from GitHub Actions. Through all of it the app has stayed plain HTML, CSS, and JavaScript with no framework, no build step, and no runtime dependencies, so it still opens straight from disk.
The work has been run by a rotating crew of manager, coder, reviewer, and ops sessions, and almost every feature carries a tests commit and a review-fixes commit behind it.
The owner ran the two setup steps for the dev stack the same evening the paste import merged. The configuration went in cleanly; the deploy was refused by the cloud provider, which requires a one-time account verification before it will create a new content-delivery distribution. The stack rolled back, the live site is untouched, and the dev site now waits on the provider rather than on code. The owner chose to wait a day or two and retry before opening a support case.
The import path for sites that refuse every app is in. View the recipe page's source in your browser, copy it, paste it into the Add form, and the page picks out the recipe data and sends only that to the importer, which parses it without fetching anything. A one-click bookmarklet does the copying. Review caught a stripper that could be made very slow by a hostile paste; it was fixed before merge. It goes to the dev site first, and live only when the owner says so.
Every merge had gone straight to the live site since the CI pipeline landed. The owner asked for a place to preview changes first. The answer is a second copy of the whole stack with its own address, a live branch that only moves on the owner's word, and a DEV mark on the page. The paste-a-page import for sites that refuse every app is approved, on the condition that it lands on dev first.
Recipe imports were failing on ordinary food blogs. The cause was the importer's own disguise: a fake Chrome user agent that drew bot challenges. It now asks as CookbookImporter/1.0, falls back once on a refusal, and tells the user the truth about rate limits, bad links, and sites that refuse every app.
The DynamoDB schema is being replaced. Users are re-keyed to a hashed identifier, a history table replaces in-row snapshots, and an email index makes accounts findable. Three candidate layouts were compared on a read-only preview; the cut-over waits for a data freeze.
A display-only serving multiplier scales quantities without rewriting the recipe, a rule-based estimate fills in servings when a recipe has none, a one-time What's new card announces changes, and import retries once and refuses clearly.
A code review found a path by which a signed-in user could write to another user's data. It was fixed the same day, a regression test was added, and the attack-case suite grew to cover the pattern.
A media Lambda now stores photos in S3 under per-user versioned keys, with a Pexels search and an image proxy behind an hourly limit. On the same day the Claude-backed import fallback and YouTube import were removed: Cookbook will not call an AI API, ever.
Deploys moved to GitHub Actions with OIDC, no long-lived keys, and a verify step that hashes what CloudFront serves against what was built. The database changed from one document per book to one item per recipe, with revision snapshots and an admin restore.
An eight-phase runbook took Cookbook from a local dev server to CloudFront, S3, Lambda, and DynamoDB with Google sign-in, in one long day. The same evening it opened to anyone with a Google account.
Below 720 pixels the app becomes a phone app: a sticky header that shrinks from 211 to 61 pixels, filters in a bottom sheet, a bottom nav, and PWA install. An Options panel with Theme, Fonts, and Layout tabs lets each person set the look.
Cookbook began as a single-file artifact in a chat window. On day one it became a repo with a tiny dev server, a GET and PUT API for the whole book, and its first real bug fix: rice vinegar and white vinegar were being merged into one ingredient.
Cookbook began as a single-file artifact in a chat window. On day one it became a repo with a tiny dev server, a GET and PUT API for the whole book, and its first real bug fix: rice vinegar and white vinegar were being merged into one ingredient.
Below 720 pixels the app becomes a phone app: a sticky header that shrinks from 211 to 61 pixels, filters in a bottom sheet, a bottom nav, and PWA install. An Options panel with Theme, Fonts, and Layout tabs lets each person set the look.
An eight-phase runbook took Cookbook from a local dev server to CloudFront, S3, Lambda, and DynamoDB with Google sign-in, in one long day. The same evening it opened to anyone with a Google account.
Deploys moved to GitHub Actions with OIDC, no long-lived keys, and a verify step that hashes what CloudFront serves against what was built. The database changed from one document per book to one item per recipe, with revision snapshots and an admin restore.
A media Lambda now stores photos in S3 under per-user versioned keys, with a Pexels search and an image proxy behind an hourly limit. On the same day the Claude-backed import fallback and YouTube import were removed: Cookbook will not call an AI API, ever.
A code review found a path by which a signed-in user could write to another user's data. It was fixed the same day, a regression test was added, and the attack-case suite grew to cover the pattern.
A display-only serving multiplier scales quantities without rewriting the recipe, a rule-based estimate fills in servings when a recipe has none, a one-time What's new card announces changes, and import retries once and refuses clearly.
The DynamoDB schema is being replaced. Users are re-keyed to a hashed identifier, a history table replaces in-row snapshots, and an email index makes accounts findable. Three candidate layouts were compared on a read-only preview; the cut-over waits for a data freeze.
Recipe imports were failing on ordinary food blogs. The cause was the importer's own disguise: a fake Chrome user agent that drew bot challenges. It now asks as CookbookImporter/1.0, falls back once on a refusal, and tells the user the truth about rate limits, bad links, and sites that refuse every app.
Every merge had gone straight to the live site since the CI pipeline landed. The owner asked for a place to preview changes first. The answer is a second copy of the whole stack with its own address, a live branch that only moves on the owner's word, and a DEV mark on the page. The paste-a-page import for sites that refuse every app is approved, on the condition that it lands on dev first.
The import path for sites that refuse every app is in. View the recipe page's source in your browser, copy it, paste it into the Add form, and the page picks out the recipe data and sends only that to the importer, which parses it without fetching anything. A one-click bookmarklet does the copying. Review caught a stripper that could be made very slow by a hostile paste; it was fixed before merge. It goes to the dev site first, and live only when the owner says so.
The owner ran the two setup steps for the dev stack the same evening the paste import merged. The configuration went in cleanly; the deploy was refused by the cloud provider, which requires a one-time account verification before it will create a new content-delivery distribution. The stack rolled back, the live site is untouched, and the dev site now waits on the provider rather than on code. The owner chose to wait a day or two and retry before opening a support case.