Meal-prep planner with weekly dinners and shopping lists
Subscription app that builds weekly five-dinner plans from stored recipes, with merged shopping lists, JSON export, a recipe editor, and per-user locale and
sveltekittypescriptsqlitebetter-sqlite3stripevitest
Built 1 time, downloaded 0 times.
Install from your terminal
Run this in your project folder. It signs you in, shows what changes and asks before it overwrites a file.
npx claude-lab add meal-prep-planner-with-weekly-dinners-and-shopping-lists-d7115aCLAUDE.md# Meal-prep planner
## Purpose
- Subscription app for busy parents. Every Sunday it builds a plan of 5 dinners, an aisle-grouped shopping list and a one-hour prep schedule.
- Plans use ONLY recipes stored in the database. Never invent recipes, ingredients or prices.
- The owner is a backend developer, strong in SQL and weaker in frontend. When you introduce a Svelte 5 rune, a `load` function or a form action, add one line explaining the choice.
- Prefer simple server-rendered pages with progressive enhancement.
## Stack
- SvelteKit with TypeScript and Svelte 5.
- better-sqlite3 with plain SQL migrations in `/migrations`. No ORM.
- Vitest for unit tests. Playwright for e2e at a 390px mobile viewport.
- Paraglide JS for UI messages, two locales at launch.
- Stripe for billing. zod validates all form input.
- Runes (`$state`, `$derived`) hold component state. `load` functions fetch data on the server before render. Form actions handle POST, so forms work without JavaScript.
## Commands
- `npm run dev`: start the dev server
- `npm run check`: svelte-check
- `npm run lint`: ESLint and Prettier checks
- `npm test`: Vitest unit tests
- `npm run test:e2e`: Playwright e2e tests
- `npm run build`: production build
- `npm run db:migrate`: apply SQL migrations
- `npm run db:seed`: insert seed data
- `npm run job:weekly`: run the weekly plan job
## Directory map
- `src/lib/server/db`: queries, one file per aggregate
- `src/lib/server/planner`: generator and swap logic
- `src/lib/server/shopping`: merge, convert and export
- `src/lib/units`, `src/lib/money`: pure helpers with unit tests
- `src/routes/(app)`: user pages
- `src/routes/admin`: recipe editor
- `src/routes/api/v1`: JSON endpoints
## Non-negotiable rules
- Money is integer minor units plus an ISO currency code. Never use floats.
- Quantities are stored in canonical metric (g, ml, count). Convert only for display.
- User-facing text goes through Paraglide messages. No hard-coded strings.
- Every user has locale, country, currency, unit_system and timezone columns.
- Base portion is 4 servings. Recipes scale by household size.
- Diet tags (vegetarian, nut-free, halal) are derived from ingredient flags (contains_meat, pork, alcohol, nuts, animal_product). Never hand-type them.
- The shopping list is derived from plan rows on read and never stored. Swapping one dinner needs no regeneration.
- Merge shopping items by ingredient_id plus canonical unit, then convert for display.
- The JSON export is versioned (`version: 1`) and documented in SPEC.md.
- The planner is deterministic given a seed. Filter diets and allergens in SQL. Respect the weekly budget. Avoid repeats from the last 4 weeks. Keep combined prep under 60 minutes. Each rule has a unit test.
## Gotchas
- Every SQLite connection needs `PRAGMA journal_mode=WAL` and `PRAGMA foreign_keys=ON`.
- Run the weekly job per user timezone, not server time.
- Stripe webhooks must verify the signature and be idempotent.
- The trial is 7 days.
- Never edit an applied migration. Use the add-migration skill.
## Security
- Never commit secrets. Real values stay in `.env`, which is never committed.
- `.env.example` holds placeholders only: `STRIPE_SECRET_KEY=YOUR_STRIPE_KEY`, `EMAIL_API_KEY=YOUR_EMAIL_KEY`.
- Admin routes check the role in `hooks.server.ts`.
- Every API endpoint checks ownership. Users read only their own plans and lists.
## Workflow
- Explore, plan, code, then run the `verify` skill before saying a task is done.
- Use `/clear` between unrelated tasks.
- Use the test-runner subagent (haiku) for long test output.
- Skills: `verify`, `shopping-list-rules` (loads for shopping and unit files), `add-migration`.
- Phase prompts are in PLAN.md. Read SPEC.md only when you need the product detail.
- A PostToolUse hook runs prettier on every edited file.
## Review rules
@REVIEW.md
.claude/agents/test-runner.md---
name: test-runner
description: Use to run unit or e2e tests (npm test, npm run test:e2e) and report only pass/fail counts and failures. Keeps long test output out of the main context.
model: haiku
tools: Bash, Read, Grep, Glob
disallowedTools: Edit, Write
maxTurns: 12
---
You run the test command you are given and report the results briefly.
## Steps
1. Run the requested command exactly, for example `npm test` or `npm run test:e2e`. Add no flags unless asked.
2. If the output is long, read the summary lines and grep for `FAIL`, `Error` or `expected` instead of printing everything.
3. Open a failing file only if you need its line numbers.
## Report format
- Command: the exact command run
- Result: pass or fail, with pass and fail counts
- For each failure: test name, file path with line number, first 3 error lines
- Likely cause: one line per failure
## Rules
- Never edit, create or delete files. Never change tests to make them pass.
- Do not print full logs or names of passing tests.
- If the command cannot start (missing dependency, no server), say so in one line and stop.
.claude/settings.json{
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Bash(npm run check)",
"Bash(npm run lint)",
"Bash(npm test)",
"Bash(npm run test:e2e)",
"Bash(npm run build)",
"Bash(npm run db:migrate)",
"Bash(npm run db:seed)",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)"
],
"ask": [
"Bash(git push:*)",
"Bash(npm install:*)",
"Bash(git commit:*)"
],
"deny": [
"Read(./.env)",
"Read(./.env.local)",
"Read(./.env.development)",
"Read(./.env.test)",
"Read(./.env.staging)",
"Read(./.env.production)",
"Bash(rm -rf:*)",
"Bash(sudo:*)",
"Bash(curl * | sh)",
"Bash(curl * | bash)",
"Bash(wget * | sh)",
"Bash(sqlite3 * DROP:*)",
"Write(./data/production/**)",
"Edit(./data/production/**)",
"Write(./data/prod/**)",
"Edit(./data/prod/**)"
],
"blockWritesOutsideWorkingDirectories": true
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "f=$(jq -r '.tool_input.file_path // empty'); [ -n \"$f\" ] && npx --no-install prettier --write \"$f\" >/dev/null 2>&1 || true"
}
]
}
]
}
}
.claude/skills/add-migration/SKILL.md---
name: add-migration
description: Add a SQLite schema migration safely. Use when a task changes the database schema, such as new tables, columns, indexes or constraints.
disable-model-invocation: true
argument-hint: short-name
---
# Add migration
Argument: `$ARGUMENTS` is the short name, for example `add-recipe-photos`.
## Steps
1. Find the highest number in `migrations/`. Use the next number, zero-padded to four digits.
2. Create `migrations/NNNN_short-name.sql`.
3. Prefer additive changes: new tables, new nullable columns, new indexes.
4. Update the query files in `src/lib/server/db` (one file per aggregate) and their types.
5. Update the seed script if the new data needs seeding.
6. Run `npm run db:migrate` against a new empty temp SQLite file. Confirm it exits 0.
7. Run `npm test`.
8. Add or update a test that covers the new columns or constraints.
## SQLite caveats
- `ALTER TABLE ADD COLUMN` cannot add a column with `PRIMARY KEY` or `UNIQUE`. It cannot add `NOT NULL` without a default. It cannot add a column with a foreign key default that is not NULL.
- Changing a column type, constraint or default needs the create-new-copy-rename pattern:
1. `CREATE TABLE <name>_new (...)` with the new definition.
2. `INSERT INTO <name>_new SELECT ... FROM <name>`.
3. `DROP TABLE <name>`.
4. `ALTER TABLE <name>_new RENAME TO <name>`.
5. Recreate indexes and triggers.
- Keep `foreign_keys=ON`. Foreign key enforcement cannot be turned off inside a transaction. Check FK references after the copy.
- Write a rollback note in the migration header comment if the change cannot be reversed by a simple statement.
## Rules
- Never edit a migration that has already been applied. Add a new migration instead.
- Keep migration SQL free of data that contains secrets.
- Use parameterized queries in all query code.
.claude/skills/shopping-list-rules/SKILL.md---
name: shopping-list-rules
description: Use when working on the shopping list, unit conversion, ingredient merging, rounding, aisle grouping or the JSON export. Covers the domain rules for all of them.
paths:
- src/lib/shopping/**
- src/lib/units/**
---
# Shopping list rules
## Canonical units
- Store and merge in canonical units: `g` (mass), `ml` (volume), `count` (pieces).
- Convert only for display.
## Conversions
| From | To canonical | Notes |
|---|---|---|
| kg | 1000 g | |
| oz | 28.3495 g | |
| lb | 453.592 g | |
| l | 1000 ml | |
| tsp | 5 ml | |
| tbsp | 15 ml | |
| cup | 240 ml | metric |
| cup | 236.6 ml | US |
- Cups and spoons for solid ingredients need `density_g_per_ml` to convert volume to grams.
- If density is missing, keep the original unit and flag the item. Never guess a density.
## Merging
- Merge key: `ingredient_id` plus unit dimension (mass, volume, count).
- Sum in canonical units first. Convert to the display unit last.
## Scaling
- Scale each recipe ingredient by `plan_items.servings / recipes.base_servings`.
## Rounding
- Under 100 g: round up to the next 5 g.
- 100 g and above: round up to the next 50 g.
- Count: round up to whole pieces.
## Grouping and sorting
- Group items by aisle, using the aisle name in the user's locale.
- Sort items alphabetically within each aisle.
## Export
- Field names and versioning follow SPEC.md.
- Bump the `version` field on any breaking change, and update REVIEW.md if the rule changes.
## Tests
- Golden fixtures live in `tests/fixtures/shopping/*.json`.
- Add one fixture for each new rule.
- Unit-test every conversion, including the missing-density case.
.claude/skills/verify/SKILL.md---
name: verify
description: Run before saying any task is done. Proves a change works with type check, lint, tests, build, a smoke run and a fresh migration check.
allowed-tools: Bash(npm run check) Bash(npm run lint) Bash(npm test) Bash(npm run build) Bash(npm run test:e2e) Bash(npm run dev) Bash(npm run db:migrate) Bash(git status) Bash(git diff:*) Bash(git log:*)
---
# Verify
Run the steps in order. If a step fails, fix the cause, then rerun from that step.
## Checks
1. `npm run check` (svelte-check).
2. `npm run lint`.
3. `npm test` (Vitest unit tests).
4. `npm run build`.
5. `npm run test:e2e`, only if routes or UI changed. Check with `git status` and `git diff --name-only`. Run it if any `.svelte` file, anything under `src/routes`, or `messages/` (Paraglide) changed.
## Migration check
1. Point the migration runner at a new empty temp SQLite file. Set the database path variable that the runner reads; check `src/lib/server/db` for its name.
2. Run `npm run db:migrate`.
3. Confirm it exits 0.
## Smoke run
1. Start `npm run dev` in the background. Wait until it prints the local URL.
2. Request `/api/health`, which should return 200.
3. Request the shopping list JSON for the seed user: `/api/v1/plans/:id/shopping-list.json`. Use the seed user's plan id from the seed script.
4. Confirm the JSON contains `"version": 1`.
5. Stop the server.
`curl` and stopping the server are not in allowed-tools. If a prompt appears, approve it for this run only. Do not add it to settings.
## Report
List each command with PASS or FAIL. For a failure, show only the first failing lines. Example:
```
check PASS
lint PASS
test FAIL first error lines here
```
## Rules
- Never skip a check.
- Never edit or delete tests to make them pass.
- If a check cannot run, report it as NOT RUN with the reason. Do not say the task is done.
- Say "done" only when every required step passes.
INSTALL.md# Install and first run
Setup guide for starting the meal-prep planner with Claude Code.
## Prerequisites
- Node 20 or newer.
- git.
- Claude Code 2.1.296 or newer. Check with `claude --version`.
- A Stripe test account. Use test keys only until Phase 5.
## Copy the files
1. Create the repo folder and run `git init`.
2. Copy the files from this plan into the repo root.
3. Scaffold the app with `npm create svelte@latest .`. Choose TypeScript, Playwright, Vitest, ESLint and Prettier. Or leave this to Phase 1.
## Install plugins
```
claude plugin install code-review@claude-plugins-official
claude plugin install security-guidance@claude-plugins-official
claude plugin marketplace add anthropics/skills
claude plugin install example-skills@anthropic-agent-skills
```
`example-skills` includes frontend-design and webapp-testing.
## Set up the environment file
1. Copy `.env.example` to `.env`.
2. Keep the placeholders `YOUR_STRIPE_KEY` and `YOUR_EMAIL_KEY` until Phase 5.
3. Never commit `.env`. The project deny rules block Claude from reading it.
## Start Claude Code
1. Run `claude` in the repo root.
2. Run `/skill-doctor` to confirm the verify, shopping-list-rules and add-migration skills load.
3. Open PLAN.md, copy the Phase 1 `/goal` prompt and paste it.
4. Check progress with `/usage`.
## Run each phase
- Run one phase per session.
- When a phase passes, run the `verify` skill, commit, then `/clear`.
- Start the next phase from PLAN.md.
- Use `/compact` at natural breaks inside a long phase.
- The test-runner subagent returns only failures from long test output.
## Editor access for the nutritionist
- Run the app locally with `npm run dev`, then open `/admin` and sign in with the seed admin login.
- The seed admin password comes from `.env`. It is a placeholder until you change it.
- Change the seed password before sharing any environment.
- Staging access follows the deployment notes written in Phase 6.
## Decisions to make early
- Hosting with a persistent disk for SQLite.
- Email provider for sign-up and weekly emails.
- Ingredient price source for each country.
- Make these decisions before Phase 5.
PLAN.md# Build plan: meal-prep planner
## How to use this plan
- Run one phase per session. Run `/clear` between phases.
- Each phase ends with its `/goal` prompt. The goal runs until the check passes or the turn bound is reached.
- Every `/goal` prompt is under 3,500 characters and includes an end state, a check, constraints and a turn bound.
- Use default effort everywhere except the Phase 3 planner design (see Phase 3).
- Run the `verify` skill before you mark any phase done.
- Commit at the end of each phase.
## Phase 1: Foundation
**Files to touch:** repo root scaffold, `package.json`, `svelte.config.js`, `migrations/`, `src/lib/server/db/`, `src/lib/units/`, `src/lib/money/`, Paraglide messages, `src/hooks.server.ts`, `src/routes/` for home, login and signup, `src/routes/admin/`, `src/routes/api/health/`, `.env.example`, `scripts/`.
**Do not:**
- Build recipe or planner features.
- Add UI beyond home, login, signup and an empty admin page.
- Use an ORM.
- Commit secrets. Use placeholders in `.env.example` only.
**Goal:**
```
/goal Phase 1 Foundation of the meal-prep planner. Read CLAUDE.md and SPEC.md first. End state: (1) SvelteKit TypeScript app scaffolded in the repo root with Vitest, Playwright, ESLint, Prettier; (2) better-sqlite3 connection helper that sets WAL and foreign_keys=ON; plain SQL migration runner (`npm run db:migrate`) and migrations in /migrations creating users, profiles (locale, country, currency, unit_system, timezone, household_size, budget_minor), sessions, ingredients (flags, aisle_id, density_g_per_ml), ingredient_translations, aisles, aisle_translations, recipes, recipe_translations, recipe_ingredients, recipe_steps, plans, plan_items, subscriptions; (3) `npm run db:seed` inserts 2 aisles, 15 ingredients with translations in two locales, and one admin user with a placeholder password from env; (4) src/lib/units with canonical conversion (g, ml, count, kg, l, tsp, tbsp, cup metric and US, oz, lb) and src/lib/money with integer minor unit formatting per locale and currency, both with unit tests covering edge cases and missing density; (5) Paraglide JS set up with two locales and a locale switcher on a simple home page; (6) email/password signup and login with argon2 hashed passwords, session cookie, role check for /admin in hooks.server.ts, and a GET /api/health route. Check: `npm run check`, `npm run lint`, `npm test`, `npm run build` all exit 0, and `npm run db:migrate` succeeds on a fresh temp database file. Constraints: no secrets in code, use .env.example placeholders; no ORM; no UI beyond home, login, signup and an empty admin page; do not build recipe or planner features yet; add dependencies only for sveltekit tooling, better-sqlite3, @node-rs/argon2, zod, paraglide; keep money as integer minor units; every user-facing string goes through i18n. Run the verify skill when finished and show its report in the conversation so the result can be judged. Stop after at most 40 turns and, if not finished, summarise what remains.
```
## Phase 2: Recipes admin
**Files to touch:** `src/lib/server/db/ingredients.ts`, `src/lib/server/db/recipes.ts`, `src/lib/server/db/diet.ts` (derivation only), `src/routes/admin/` for ingredient and recipe CRUD and the editor, a new migration made with the `add-migration` skill if the schema changes, `tests/e2e/` for the recipe test, `messages/` for both locales.
**Do not:**
- Store diet tags as editable fields. They must be derived from ingredient flags.
- Build public recipe browsing.
- Add user-submitted or AI-generated recipes.
- Edit an applied migration.
**Goal:**
```
/goal Phase 2 Recipes admin of the meal-prep planner. Read CLAUDE.md, SPEC.md and REVIEW.md first. End state: (1) ingredient CRUD and recipe CRUD under /admin with role check; (2) diet tags (vegetarian, nut-free, halal) derived from ingredient flags in one tested function, never editable; (3) recipe translations and ingredient translations for both locales, with tabs in the editor; (4) draft and publish status, with only published recipes visible outside admin; (5) editor form with ingredient autocomplete, portions per 4 servings, step list with minutes and batchable flag, and a live preview at 390px width; (6) a Playwright test at 390px viewport that creates a recipe, adds ingredients and steps, and publishes it. Check: `npm test`, `npm run test:e2e` and `npm run check` all exit 0. Constraints: parameterized SQL only; quantities stored canonical metric; money as integer minor units; every user-facing string through i18n in both locales; validation messages in plain language; no user-submitted or AI-generated recipes; no migration edits after apply. Run the verify skill when finished and show its report in the conversation. Stop after at most 50 turns and, if not finished, summarise what remains.
```
## Phase 3: Planner
**Before coding:** in the first message of this phase only, type `ultrathink` and ask for the generator and swap design. Use default effort for the rest of the phase.
**Files to touch:** `src/lib/server/planner/generator.ts`, `src/lib/server/planner/swap.ts`, `src/lib/server/db/plans.ts`, `src/routes/(app)/plan/`, `src/routes/api/v1/plans/`, `tests/fixtures/` (40-recipe seed), `src/lib/server/planner/*.test.ts`, `tests/e2e/swap.spec.ts`.
**Do not:**
- Build the shopping list yet.
- Use an LLM or any non-deterministic randomness. Use the seed.
- Change the units or money modules beyond calling them.
**Goal:**
```
/goal Phase 3 Planner of the meal-prep planner. Read CLAUDE.md and SPEC.md (planner and swap rules) first. End state: (1) deterministic generator that builds a 5-dinner plan for a user's week from published recipes, given a seed; (2) rules: SQL diet and allergen filter, estimated cost within weekly budget, no repeat from the last 4 weeks, combined active prep under 60 minutes; (3) clear error when too few recipes match, naming the filter; (4) plan view page under (app) showing 5 slots with recipe, servings, cost and prep minutes; (5) swap endpoint and button that replaces one plan_items row under the swap rule; (6) seeded fixture of 40 recipes in tests/fixtures; (7) one unit test per rule plus a determinism test (same seed, same plan); (8) a Playwright swap test at 390px. Check: `npm test`, `npm run test:e2e` and `npm run check` all exit 0. Constraints: parameterized SQL; users can read only their own plans; money as integer minor units; no hard-coded strings; no shopping list code; no randomness without the seed. Run the verify skill when finished and show its report in the conversation. Stop after at most 45 turns and, if not finished, summarise what remains.
```
## Phase 4: Shopping list
**Files to touch:** `src/lib/server/shopping/` (merge, convert, group, export), `src/routes/(app)/plan/[id]/shopping/`, `src/routes/(app)/plan/[id]/prep/`, `src/routes/api/v1/plans/[id]/shopping-list.json/`, `tests/fixtures/shopping/*.json`, shopping unit and schema tests, `tests/e2e/`.
**Keep** shopping code in `src/lib/server/shopping/` as CLAUDE.md specifies.
**Do not:**
- Store the shopping list. It is derived from plan_items on read.
- Guess a density when one is missing. Keep the unit and flag it.
- Change the export version without documenting it in SPEC.md.
**Goal:**
```
/goal Phase 4 Shopping list of the meal-prep planner. Read CLAUDE.md, SPEC.md (shopping list rules and JSON export contract) and the shopping-list-rules skill first. End state: (1) merge and convert module that merges duplicates by ingredient_id plus unit dimension, sums in canonical units, then converts for display; (2) rounding to purchasable amounts as the skill defines; (3) aisle grouping in the user's locale, items sorted alphabetically within each aisle; (4) mobile checklist UI at 390px with check-off persisted per user; (5) GET /api/v1/plans/:id/shopping-list.json returning the contract in SPEC.md with version 1, with a schema test; (6) prep-schedule page showing active prep minutes per step; (7) golden fixtures in tests/fixtures/shopping/ with one per new rule. Check: `npm test` including golden fixtures, `npm run test:e2e` and `npm run check` all exit 0. Constraints: derive the list on read; missing density keeps the original unit and flags the item; parameterized SQL; users read only their own lists; no hard-coded strings. Run the verify skill when finished and show its report in the conversation. Stop after at most 40 turns and, if not finished, summarise what remains.
```
## Phase 5: Billing and Sunday job
**Files to touch:** `src/lib/server/billing/` (checkout, portal, webhook handling), `src/routes/api/v1/stripe/webhook/`, `src/routes/(app)/billing/`, subscription gate in `src/hooks.server.ts` or a shared guard, `src/lib/server/jobs/weekly.ts`, `scripts/job-weekly.ts`, a migration for processed Stripe event IDs made with `add-migration`, tests with mocked Stripe.
**Do not:**
- Put real Stripe keys in code, logs or fixtures. Use `.env.example` placeholders.
- Make live charges or call live Stripe in tests.
- Store card data.
- Send real email. Use the stub.
- Run the weekly job on server time. Run it per user timezone.
**Goal:**
```
/goal Phase 5 Billing and Sunday job of the meal-prep planner. Read CLAUDE.md, SPEC.md (billing) and REVIEW.md first. End state: (1) Stripe checkout session with a 7-day trial per currency and country price; (2) customer portal link; (3) webhook endpoint that verifies the Stripe signature and is idempotent by storing processed event IDs; (4) subscription gate on (app) routes that redirects users without an active or trialing subscription to billing; (5) grace handling for failed payments with a visible banner; (6) `npm run job:weekly` that generates each user's plan at the start of their local Sunday in their timezone, sends the weekly email through the email stub, and skips users who already have a plan for that week; (7) tests with mocked Stripe for checkout, webhook signature failure, duplicate webhook delivery and the timezone rule. Check: `npm test`, `npm run check` and `npm run build` all exit 0. Constraints: secrets only from env with placeholders in `.env.example`; no live Stripe calls in tests; no card data stored; no real email sent; parameterized SQL; no server-time scheduling. Run the verify skill when finished and show its report in the conversation. Stop after at most 40 turns and, if not finished, summarise what remains.
```
## Phase 6: Hardening and launch
**Files to touch:** `src/lib/server/rate-limit/`, `scripts/backup-sqlite.sh`, `scripts/restore-sqlite.sh`, `tests/restore/`, `DEPLOY.md`, `README.md`, and small fixes wherever the review finds issues.
**Do not:**
- Add new product features.
- Add third-party analytics.
- Disable or skip security checks to pass.
**Goal:**
```
/goal Phase 6 Hardening and launch of the meal-prep planner. Read CLAUDE.md and REVIEW.md first. End state: (1) security review with /code-review against REVIEW.md, with every finding fixed or recorded with a reason in the conversation; (2) rate limits on login, signup and all API routes, with tests; (3) accessibility and layout pass at 390px: no horizontal scroll, tap targets at least 44px, Lighthouse accessibility score of at least 90 on home, login, plan and shopping pages; (4) scripts/backup-sqlite.sh that makes a consistent backup using SQLite's backup API, and scripts/restore-sqlite.sh; (5) a restore test that restores a backup into a temp file and checks row counts for users, plans and recipes; (6) DEPLOY.md with hosting, persistent disk, environment variables by name only, and backup schedule; (7) README.md with setup and commands. Check: the verify skill passes and the restore test passes. Constraints: no new product features; no third-party analytics; no skipped or disabled checks; no secrets committed. Show the verify report and the review summary in the conversation. Stop after at most 35 turns and, if not finished, summarise what remains.
```
REVIEW.md# Review checklist
Applied by /code-review through the `@REVIEW.md` import in CLAUDE.md. Check every item against changed files.
## Money and units
- Money is integer minor units with an ISO currency code. No floats in arithmetic.
- Quantities are stored as canonical metric (g, ml, count). Conversion happens only at display, and it has a test.
## Text and i18n
- No hard-coded user-facing strings. Both locales have the new key.
## Data and SQL
- Every SQL query is parameterized. No string concatenation into queries.
- Migrations are additive and have a test or a documented rollback.
## Access control
- Admin routes and API endpoints check role and ownership.
- A user can read only their own plan and shopping list.
## Billing
- Stripe webhooks verify the signature and are idempotent.
- No secrets in code, logs, or committed files.
## Planner
- Planner changes keep seed-based determinism.
- Every changed planner rule has a test.
## UI
- Works at 390px width with no horizontal scroll.
- Tap targets are at least 44px.
## API contract
- The shopping list JSON `version` is bumped on any breaking change.
## Out of scope
- Skip style nits that prettier and eslint already enforce.
SPEC.md# Product spec: meal-prep planner
Starter draft. Claude and the owner refine this before Phase 1 locks the schema.
## Users and goals
- Busy parents who plan dinners on Sunday and shop on mobile in the supermarket.
- Owner: backend developer who builds and runs the app.
- Partner: nutritionist who writes and publishes recipes in the admin editor.
- Goal: five dinners a week, one shopping list, one prep hour. No invented recipes.
## Core flows
1. Sign up and start a 7-day trial.
2. Set profile: diet filters, household size, weekly budget, locale, country, currency, unit system, timezone.
3. Sunday plan: generated from published recipes for the coming week.
4. View plan: five slots with recipe, servings, estimated cost and active prep minutes.
5. Swap one dinner for a replacement.
6. Open the shopping list grouped by aisle. Check off items; the state is kept on the device when offline and synced when back online.
7. Export the shopping list as JSON.
## Data model sketch
- users(id, email, password_hash, role, created_at)
- profiles(user_id, locale, country, currency, unit_system, timezone, household_size, budget_minor)
- ingredients(id, flags, aisle_id, density_g_per_ml)
- ingredient_translations(ingredient_id, locale, name)
- ingredient_prices(ingredient_id, country, price_minor per 100 g)
- aisles(id, sort_order) + aisle_translations(aisle_id, locale, name)
- recipes(id, status draft|published, base_servings, prep_minutes_active)
- recipe_translations(recipe_id, locale, title, notes)
- recipe_ingredients(recipe_id, ingredient_id, quantity_canonical, unit)
- recipe_steps(recipe_id, position, text per locale, prep_minutes, batchable)
- plans(id, user_id, week_start)
- plan_items(id, plan_id, slot 1-5, recipe_id, servings)
- subscriptions(id, user_id, stripe_customer_id, stripe_subscription_id, status, trial_end)
Rules for the model:
- Money is integer minor units with an ISO currency code. Never floats.
- Quantities are stored in canonical metric units (g, ml, count). Conversion happens only for display.
- Diet tags are not stored. They are derived from ingredient flags: contains_meat, pork, alcohol, nuts, animal_product.
- Recipe base portion is 4 servings. Quantities scale by household size.
## Planner rules
- Filter recipes by diet and allergens in SQL.
- Keep the estimated cost of the five dinners within the weekly budget.
- Do not repeat any recipe from the last 4 weeks.
- Keep combined active prep under 60 minutes.
- The same seed always produces the same plan.
- If too few recipes match, return a clear error that names the filter that caused it.
## Swap rule
- The replacement must match the same diet and allergen filters and the same budget.
- The replacement must not already be in the week.
- A swap updates one plan_items row only. Nothing else in the plan changes.
## Shopping list rules
- The list is derived from plan_items on read. It is never stored.
- Merge duplicates by ingredient_id plus unit dimension (mass, volume, count).
- Convert units (g, kg, ml, l, cup, tbsp, tsp, piece, oz, lb) after summing in canonical units.
- Cups and spoons for solids need density_g_per_ml. If it is missing, keep the original unit and flag the item. Never guess.
- Scale each line by plan_items.servings divided by recipe base_servings.
- Round to purchasable amounts as defined in the shopping-list-rules skill.
- Group by aisle, using the aisle name in the user's locale. Sort items alphabetically within each aisle.
## JSON export contract
GET /api/v1/plans/:id/shopping-list.json returns:
```
{
"version": 1,
"week_start": "YYYY-MM-DD",
"locale": "xx",
"currency": "ISO code",
"household_size": 4,
"items": [
{
"ingredient_id": 1,
"name": "string in user locale",
"aisle": "string in user locale",
"quantity_canonical": 0,
"unit_canonical": "g | ml | count",
"quantity_display": 0,
"unit_display": "string",
"estimated_price_minor": 0
}
]
}
```
- Bump `version` on any breaking change. Document every change here.
- Users can read only their own plans and lists.
## Admin recipe editor
- Simple form for a non-technical user.
- Ingredient autocomplete.
- Portions shown per 4 servings.
- Step list with minutes and a batchable flag per step.
- Photo upload.
- Side-by-side live preview at mobile width.
- Draft and publish buttons.
- Translation tabs, one per language.
- Validation messages in plain language.
- Diet tags shown as read-only, derived from the ingredients.
## Billing
- Monthly Stripe subscription with a 7-day trial.
- Price set per currency and country.
- Customer portal link for changing payment details or cancelling.
- Failed payments: short grace period with a clear banner before access is gated.
- Webhooks are signature-verified and idempotent.
## Launch scope
- Two countries and two languages.
- Units, prices and language are set per user from day one.
## Non-goals for v1
- Delivery partner integration.
- User-submitted recipes.
- AI-generated recipes.
- Native apps.
## Open questions
- Ingredient price source for each country.
- Email provider.
- Hosting with a persistent disk for SQLite.
- Backup method and retention.