Trevor PaughSystems engineer
Trevor PaughProject Chronicles

Chronicles / Cookbook

Active · Sep 2026 to present

Cookbook

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.

12 entries Stack Vanilla HTML/CSS/JS · Node 18 ESM · AWS Lambda · DynamoDB · S3 + CloudFront · CloudFormation · GitHub Actions cookbook.kongomax.com View on timeline

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.

Latest developments

Progress

The dev site, blocked at the door

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.

Milestone

Paste the page

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.

Decision

A dev site before live, and the paste import is a yes

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.

Decision

The importer stops pretending to be a browser

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.

Decision

Layout 2: re-keying the database

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.

Progress

Serving sizes, a What's new card, and honest imports

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.

Progress

A reviewer finds a cross-user write

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.

Decision

Photos, and the decision never to call an AI API

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.

Milestone

CI/CD and one item per recipe

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.

Milestone

To AWS in three phases, then open to the public

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.

Design

The phone shell and the Options panel

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.

Milestone

From a chat artifact to a repository

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.

The story, from the beginning

From a chat artifact to a repository

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.

Read the entry

The phone shell and the Options panel

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.

Read the entry
A recipe card as it appears today, with a user-uploaded photo.
A recipe card as it appears today, with a user-uploaded photo.

To AWS in three phases, then open to the public

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.

Read the entry
A push to main is a deploy. If that sentence is not true, the pipeline is broken.

CI/CD and one item per recipe

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.

Read the entry

Photos, and the decision never to call an AI API

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.

Read the entry
User photos upload through the API, never through presigned URLs, so every upload is checked before it lands.
User photos upload through the API, never through presigned URLs, so every upload is checked before it lands.

A reviewer finds a cross-user write

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.

Read the entry

Serving sizes, a What's new card, and honest imports

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.

Read the entry

Layout 2: re-keying the database

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.

Read the entry
I hate the current schema. It is hard to navigate and identify things.

The importer stops pretending to be a browser

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.

Read the entry
It generally fails quite a lot.

A dev site before live, and the paste import is a yes

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.

Read the entry
Whatever the simplest and easiest to manage setup is.

Paste the page

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.

Read the entry
Yes, but that should end up in dev first.

The dev site, blocked at the door

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.

Read the entry