Vibecode Pestle
track this build3 phases, 9 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Pestle, save recipes, guide cooking steps, plan meals, and share a household list. The hard boundary is native apple polish, recipe import, hands-free mode, sync, and family sharing, plus sync, integrations, and household adoption.
You are building a lean indie version of Pestle. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Pestle · indie build A household recipe box whose hard problems are a cooking mode that keeps working when the browser throttles it, a shopping list two people can tick offline without losing an item, and keeping the whole box portable enough to leave. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, installed to the home screen as a local-first web app | the kitchen is where connectivity is worst and a recipe must open anyway | | Storage | SQLite on a small server for the household, with a local copy in the browser | one shared box, but every device has to work on its own for an hour | | Timers | Stored end timestamps, remaining time computed from the clock on every render | background timers are throttled and a countdown in a callback is simply wrong | | List sync | Per item state with last-writer-wins on the item, never on the list | two people shopping is the normal case and a whole-list overwrite loses food | | Portability | Whole-box export as JSON plus images, importable into an empty instance | a recipe box you cannot take with you is a rental | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the household server and the build. - Get it: nodejs.org or your version manager. - Verify: node --version prints v22 or higher - [ ] **A phone with the app installed to the home screen** · free - Why: Cooking mode, the wake lock and the timer reconciliation can only be judged on the device that will be on a worktop with wet hands. - Get it: Open the app on the phone over the local network and add it to the home screen. - Verify: A timer started and backgrounded reports correctly on return - [ ] **A small always-on machine on your network, with a certificate** · free on hardware you own, or a few pounds a month - Why: The household needs one place to agree with, and the wake lock and installability both want a secure origin. - Get it: Any small machine or home server with a local certificate, or a cheap instance with a real one. - Verify: Two devices reach the same household over a valid certificate - [ ] **A second device you can take offline** · free - Why: The list merge is the part most likely to lose data and it cannot be tested with one device. - Get it: Any second phone, tablet or browser profile that can be put into aeroplane mode. - Verify: Both devices edit offline and reconcile without loss ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Scraping recipes from other people's sites. - Retailer ordering and barcode product data. - Smart home and appliance integrations. - Nutrition figures. - native Apple polish, recipe import, hands-free mode, sync, and family sharing - frictionless family sync - retailer and smart-home integrations - large recipe or product database - mobile polish If one of those is essential to you, that is the reason to keep paying for Pestle, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Pestle The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a household recipe box with a cooking mode whose timers are still right when you come back to the phone to replace Pestle. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The cooking screen is the part people judge this on, and the browser is actively unhelpful there. Two published behaviours decide the design. A screen wake lock is released automatically when the document stops being active, so the documented pattern is to listen for the visibility change event and request a new lock when the page becomes visible again. Support is recent and the platform can refuse a lock outright, on low battery or in a power saving mode, so the code has to work when the request fails. And background timers are throttled: a page that has been invisible for more than five minutes and silent for more than thirty seconds has its timers checked about once a minute in one major browser, another gives inactive desktop tabs a one second floor and inactive tabs on mobile a fifteen minute floor and may unload them entirely. Check the current numbers yourself. So a timer here is never a countdown living in a callback. It is an end time stored in the database. The display computes the remaining time from the clock on every render, and on load and on every return to visibility the app reconciles: anything whose end time has passed is shown as finished, with how long ago. The alarm is a best effort and the interface says so in one sentence, because a tool that silently fails to ring while somebody's stock reduces to tar has done harm. The second rule is smaller: this is a household, so two people will edit the same list at the same time and one of them will be in a supermarket with no signal. Never resolve that by taking the most recent whole list. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, installed to the home screen as a local-first web app · the kitchen is where connectivity is worst and a recipe must open anyway - Storage: SQLite on a small server for the household, with a local copy in the browser · one shared box, but every device has to work on its own for an hour - Timers: Stored end timestamps, remaining time computed from the clock on every render · background timers are throttled and a countdown in a callback is simply wrong - List sync: Per item state with last-writer-wins on the item, never on the list · two people shopping is the normal case and a whole-list overwrite loses food - Portability: Whole-box export as JSON plus images, importable into an empty instance · a recipe box you cannot take with you is a rental ### Data model (create this before Phase 1) - recipes(id, title, source_url, source_name, servings, notes, created_by, updated_at) - ingredients(recipe_id, position, quantity, unit, item, note) · structured rather than a free text line, because the list depends on it - steps(recipe_id, position, text, timer_seconds) · a step may carry a suggested duration that becomes a timer - cooks(id, recipe_id, started_at, current_step, finished_at) · one run through a recipe, so you can leave and come back - timers(id, cook_id, label, ends_at, acknowledged_at, cancelled_at) · an end time, never a remaining count - list_items(id, household_id, text, quantity, unit, source_recipe_id, state, state_changed_at, changed_by) · state is needed, in trolley or done - members(id, household_id, name, joined_at) and households(id, name, invite_code, created_at) ### Phase 1 · A box that opens in a kitchen Build: Recipes with structured ingredient rows and ordered steps, entered by hand or pasted and split. Cache everything needed to read a recipe locally so it opens offline, and show clearly when you are looking at a cached copy. Photos optional, resized on the device, with a size limit. Scaling adjusts quantities and says plainly that it does not adjust times or temperatures. Trap: Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. Done when: A recipe opens and is fully readable with the network disabled, tested in the browser's offline mode, and the cached state is shown. Ingredients are stored as quantity, unit and item in separate fields, verified in the database. Scaling changes quantities only and displays a note that times and temperatures are unchanged. A photo over the size limit is rejected with a message rather than silently resized to nothing. Do not build yet: No cooking mode, list, household or export yet. ### Phase 2 · Cooking mode, and clocks you can trust Build: A step-at-a-time view with large text and one thumb-sized control. Request a screen wake lock on entry and re-request it whenever the page becomes visible again. Timers store an end time; the display derives the remaining time from the clock. On load and on every return to visibility, reconcile every timer and show finished ones with how long ago they finished. Trap: Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. Done when: A timer started, then backgrounded past its end, shows as finished with the elapsed time on return, tested by moving the clock forward with the page hidden. The wake lock is re-requested on the visibility change event and a refused lock leaves the cooking view fully usable with a visible notice, tested by forcing rejection. No timer state is held only in memory, verified by reloading mid-cook. A cook in progress resumes at the right step after a reload. Do not build yet: No shopping list, household or export yet. ### Phase 3 · A list two people can tick at once Build: A shopping list of items with a state each, changed independently. Queue changes made offline and replay them on reconnect, merging per item by the time the state changed rather than by the time the list was written. Adding a recipe to the list merges ingredients by item and unit, and keeps a note of which recipes contributed. Deletions are tombstones, not row removals. Trap: Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. Done when: Two devices editing different items offline both keep their changes after reconnecting, tested with a simulated partition. Two devices changing the same item resolve to the later state change and the earlier one is visible in history. A deleted item does not reappear when a device that was offline reconnects, tested. Adding a recipe merges by item and unit and records which recipes contributed each line. Do not build yet: No household management, backup or export yet. ### Phase 4 · A household, and the door out Build: Join by an invite code, with every change attributed to a member and undoable. Removing a member keeps their recipes and stops their access. Export the whole household as JSON plus image files, and import it into an empty instance. Back up on a schedule with a restore exercised in tests. Trap: Exporting recipes as JSON while leaving the photos on the server means the export looks complete and is missing the only part nobody can retype, and people discover this when the server is already gone. Export images as files alongside the JSON, reference them by relative path, and test the import into an empty instance rather than assuming it. Done when: An export contains every recipe, list item and image file and imports into an empty instance with matching counts and checksums, exercised in the test suite. Removing a member revokes access immediately and leaves their contributions with their name intact. Every change is attributed to a member and can be undone from a history view. A restore from a scheduled backup reproduces the household exactly. Do not build yet: Nothing after this. ### Out of scope (and why) - Importing recipes by scraping other people's sites, which is a separate project with its own permissions and copyright questions and does not belong in a first build. - Retailer ordering and barcode product data, which is a commercial data licence rather than code. - Smart home and appliance integrations, which are a different API per manufacturer and a maintenance burden forever. - Nutrition figures, because a number computed from a database of ingredient averages and presented beside a recipe reads as measurement when it is estimate. ### README must contain - That the cooking alarm is best effort, because browsers throttle background timers, and what the current published throttling behaviour is - That the screen wake lock is released when the page is hidden and re-requested on return, and that a platform may refuse it - That every timer is stored as an end time and reconciled on load, so a backgrounded timer is never wrong - That the shopping list merges per item, and what happens when two members change the same item - How to export the whole household including images, and how to import it into a fresh instance ===== AGENTS.md ===== # Agent instructions · Pestle indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, installed to the home screen as a local-first web app, SQLite on a small server for the household, with a local copy in the browser, Stored end timestamps, remaining time computed from the clock on every render, Per item state with last-writer-wins on the item, never on the list, Whole-box export as JSON plus images, importable into an empty instance. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. - Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. - Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. ===== BUILD_PLAN.md ===== # Build plan · Pestle A household recipe box whose hard problems are a cooking mode that keeps working when the browser throttles it, a shopping list two people can tick offline without losing an item, and keeping the whole box portable enough to leave. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A box that opens in a kitchen Recipes are structured, editable and readable on a phone with no signal. ### Steps 1. Make ingredients structured from the first migration Everything downstream depends on quantity and unit existing. Files: `src/db/schema.ts`, `src/lib/ingredients.ts` 2. Cache recipes for offline reading and say so A stale copy you know about is fine; one you do not is not. Files: `src/lib/offline.ts` 3. Scale quantities, refuse to scale time Doubling a cake does not double the bake. Files: `src/lib/scale.ts`, `tests/scale.test.ts` ### Done when - [ ] A recipe reads fully offline and the cache is disclosed - [ ] Quantity, unit and item are separate fields - [ ] Scaling touches quantities only, with a note - [ ] Oversized photos are rejected with a message ### Watch out - Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. ## Phase 2 · Cooking mode, and clocks you can trust The screen stays on when it can, and every timer is correct when you come back to it. ### Steps 1. Store end times, never remaining seconds The clock is the only thing that keeps running. Files: `src/lib/timers.ts`, `tests/timers.test.ts` 2. Re-request the wake lock on visibility change The lock is dropped when the page goes away and does not come back by itself. Files: `src/lib/wakelock.ts` 3. Say plainly that the alarm is best effort One honest sentence beats a silent failure over a hot pan. Files: `src/app/cook/page.tsx` ### Done when - [ ] A backgrounded timer reports finished with elapsed time - [ ] The wake lock is re-requested on becoming visible - [ ] A refused lock leaves cooking usable with a notice - [ ] A reload resumes the cook at the right step ### Watch out - Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. ## Phase 3 · A list two people can tick at once Two members editing the list offline both keep their changes. ### Steps 1. Merge per item, never per list This is the difference between a household tool and an argument. Files: `src/lib/list-sync.ts`, `tests/list-merge.test.ts` 2. Make deletions tombstones A stale device will otherwise bring the milk back. Files: `src/lib/list.ts` 3. Record which recipe contributed each line So removing a meal from the plan can remove its items. Files: `src/lib/list.ts` ### Done when - [ ] Offline edits to different items all survive - [ ] Same-item conflicts resolve to the later state change - [ ] Deleted items do not come back from a stale device - [ ] Merged lines record their contributing recipes ### Watch out - Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. ## Not in this build - Scraping recipes from other people's sites. - Retailer ordering and barcode product data. - Smart home and appliance integrations. - Nutrition figures. ## After v1, if you want it - A weekly plan that feeds the list and can be undone as one action when the week changes - A printable list grouped by shop aisle, which is the feature people actually use in a supermarket - A pantry with rough quantities, which only becomes useful once the list merge has been trusted for a while ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recipes, lists, cooks and timers. Back this up; recipes are typed once. DATABASE_PATH=./data/household.db # Required. Recipe photos. Included in every export as files. IMAGE_DIR=./data/images # Required. Used in invite links and needed for a secure origin. PUBLIC_BASE_URL=https://kitchen.local # Required. How long a household invite code stays valid. INVITE_CODE_TTL_HOURS=48 # Required. Per photo, after the device resizes it. MAX_IMAGE_BYTES=2097152 # Required. Scheduled archives of the database and images together. BACKUP_DIR=./backups
You are building a lean indie version of Pestle. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Pestle · indie build A household recipe box whose hard problems are a cooking mode that keeps working when the browser throttles it, a shopping list two people can tick offline without losing an item, and keeping the whole box portable enough to leave. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, installed to the home screen as a local-first web app | the kitchen is where connectivity is worst and a recipe must open anyway | | Storage | SQLite on a small server for the household, with a local copy in the browser | one shared box, but every device has to work on its own for an hour | | Timers | Stored end timestamps, remaining time computed from the clock on every render | background timers are throttled and a countdown in a callback is simply wrong | | List sync | Per item state with last-writer-wins on the item, never on the list | two people shopping is the normal case and a whole-list overwrite loses food | | Portability | Whole-box export as JSON plus images, importable into an empty instance | a recipe box you cannot take with you is a rental | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the household server and the build. - Get it: nodejs.org or your version manager. - Verify: node --version prints v22 or higher - [ ] **A phone with the app installed to the home screen** · free - Why: Cooking mode, the wake lock and the timer reconciliation can only be judged on the device that will be on a worktop with wet hands. - Get it: Open the app on the phone over the local network and add it to the home screen. - Verify: A timer started and backgrounded reports correctly on return - [ ] **A small always-on machine on your network, with a certificate** · free on hardware you own, or a few pounds a month - Why: The household needs one place to agree with, and the wake lock and installability both want a secure origin. - Get it: Any small machine or home server with a local certificate, or a cheap instance with a real one. - Verify: Two devices reach the same household over a valid certificate - [ ] **A second device you can take offline** · free - Why: The list merge is the part most likely to lose data and it cannot be tested with one device. - Get it: Any second phone, tablet or browser profile that can be put into aeroplane mode. - Verify: Both devices edit offline and reconcile without loss ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Scraping recipes from other people's sites. - Retailer ordering and barcode product data. - Smart home and appliance integrations. - Nutrition figures. - native Apple polish, recipe import, hands-free mode, sync, and family sharing - frictionless family sync - retailer and smart-home integrations - large recipe or product database - mobile polish If one of those is essential to you, that is the reason to keep paying for Pestle, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Pestle The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a household recipe box with a cooking mode whose timers are still right when you come back to the phone to replace Pestle. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The cooking screen is the part people judge this on, and the browser is actively unhelpful there. Two published behaviours decide the design. A screen wake lock is released automatically when the document stops being active, so the documented pattern is to listen for the visibility change event and request a new lock when the page becomes visible again. Support is recent and the platform can refuse a lock outright, on low battery or in a power saving mode, so the code has to work when the request fails. And background timers are throttled: a page that has been invisible for more than five minutes and silent for more than thirty seconds has its timers checked about once a minute in one major browser, another gives inactive desktop tabs a one second floor and inactive tabs on mobile a fifteen minute floor and may unload them entirely. Check the current numbers yourself. So a timer here is never a countdown living in a callback. It is an end time stored in the database. The display computes the remaining time from the clock on every render, and on load and on every return to visibility the app reconciles: anything whose end time has passed is shown as finished, with how long ago. The alarm is a best effort and the interface says so in one sentence, because a tool that silently fails to ring while somebody's stock reduces to tar has done harm. The second rule is smaller: this is a household, so two people will edit the same list at the same time and one of them will be in a supermarket with no signal. Never resolve that by taking the most recent whole list. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, installed to the home screen as a local-first web app · the kitchen is where connectivity is worst and a recipe must open anyway - Storage: SQLite on a small server for the household, with a local copy in the browser · one shared box, but every device has to work on its own for an hour - Timers: Stored end timestamps, remaining time computed from the clock on every render · background timers are throttled and a countdown in a callback is simply wrong - List sync: Per item state with last-writer-wins on the item, never on the list · two people shopping is the normal case and a whole-list overwrite loses food - Portability: Whole-box export as JSON plus images, importable into an empty instance · a recipe box you cannot take with you is a rental ### Data model (create this before Phase 1) - recipes(id, title, source_url, source_name, servings, notes, created_by, updated_at) - ingredients(recipe_id, position, quantity, unit, item, note) · structured rather than a free text line, because the list depends on it - steps(recipe_id, position, text, timer_seconds) · a step may carry a suggested duration that becomes a timer - cooks(id, recipe_id, started_at, current_step, finished_at) · one run through a recipe, so you can leave and come back - timers(id, cook_id, label, ends_at, acknowledged_at, cancelled_at) · an end time, never a remaining count - list_items(id, household_id, text, quantity, unit, source_recipe_id, state, state_changed_at, changed_by) · state is needed, in trolley or done - members(id, household_id, name, joined_at) and households(id, name, invite_code, created_at) ### Phase 1 · A box that opens in a kitchen Build: Recipes with structured ingredient rows and ordered steps, entered by hand or pasted and split. Cache everything needed to read a recipe locally so it opens offline, and show clearly when you are looking at a cached copy. Photos optional, resized on the device, with a size limit. Scaling adjusts quantities and says plainly that it does not adjust times or temperatures. Trap: Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. Done when: A recipe opens and is fully readable with the network disabled, tested in the browser's offline mode, and the cached state is shown. Ingredients are stored as quantity, unit and item in separate fields, verified in the database. Scaling changes quantities only and displays a note that times and temperatures are unchanged. A photo over the size limit is rejected with a message rather than silently resized to nothing. Do not build yet: No cooking mode, list, household or export yet. ### Phase 2 · Cooking mode, and clocks you can trust Build: A step-at-a-time view with large text and one thumb-sized control. Request a screen wake lock on entry and re-request it whenever the page becomes visible again. Timers store an end time; the display derives the remaining time from the clock. On load and on every return to visibility, reconcile every timer and show finished ones with how long ago they finished. Trap: Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. Done when: A timer started, then backgrounded past its end, shows as finished with the elapsed time on return, tested by moving the clock forward with the page hidden. The wake lock is re-requested on the visibility change event and a refused lock leaves the cooking view fully usable with a visible notice, tested by forcing rejection. No timer state is held only in memory, verified by reloading mid-cook. A cook in progress resumes at the right step after a reload. Do not build yet: No shopping list, household or export yet. ### Phase 3 · A list two people can tick at once Build: A shopping list of items with a state each, changed independently. Queue changes made offline and replay them on reconnect, merging per item by the time the state changed rather than by the time the list was written. Adding a recipe to the list merges ingredients by item and unit, and keeps a note of which recipes contributed. Deletions are tombstones, not row removals. Trap: Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. Done when: Two devices editing different items offline both keep their changes after reconnecting, tested with a simulated partition. Two devices changing the same item resolve to the later state change and the earlier one is visible in history. A deleted item does not reappear when a device that was offline reconnects, tested. Adding a recipe merges by item and unit and records which recipes contributed each line. Do not build yet: No household management, backup or export yet. ### Phase 4 · A household, and the door out Build: Join by an invite code, with every change attributed to a member and undoable. Removing a member keeps their recipes and stops their access. Export the whole household as JSON plus image files, and import it into an empty instance. Back up on a schedule with a restore exercised in tests. Trap: Exporting recipes as JSON while leaving the photos on the server means the export looks complete and is missing the only part nobody can retype, and people discover this when the server is already gone. Export images as files alongside the JSON, reference them by relative path, and test the import into an empty instance rather than assuming it. Done when: An export contains every recipe, list item and image file and imports into an empty instance with matching counts and checksums, exercised in the test suite. Removing a member revokes access immediately and leaves their contributions with their name intact. Every change is attributed to a member and can be undone from a history view. A restore from a scheduled backup reproduces the household exactly. Do not build yet: Nothing after this. ### Out of scope (and why) - Importing recipes by scraping other people's sites, which is a separate project with its own permissions and copyright questions and does not belong in a first build. - Retailer ordering and barcode product data, which is a commercial data licence rather than code. - Smart home and appliance integrations, which are a different API per manufacturer and a maintenance burden forever. - Nutrition figures, because a number computed from a database of ingredient averages and presented beside a recipe reads as measurement when it is estimate. ### README must contain - That the cooking alarm is best effort, because browsers throttle background timers, and what the current published throttling behaviour is - That the screen wake lock is released when the page is hidden and re-requested on return, and that a platform may refuse it - That every timer is stored as an end time and reconciled on load, so a backgrounded timer is never wrong - That the shopping list merges per item, and what happens when two members change the same item - How to export the whole household including images, and how to import it into a fresh instance ===== AGENTS.md ===== # Agent instructions · Pestle indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, installed to the home screen as a local-first web app, SQLite on a small server for the household, with a local copy in the browser, Stored end timestamps, remaining time computed from the clock on every render, Per item state with last-writer-wins on the item, never on the list, Whole-box export as JSON plus images, importable into an empty instance. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. - Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. - Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. ===== BUILD_PLAN.md ===== # Build plan · Pestle A household recipe box whose hard problems are a cooking mode that keeps working when the browser throttles it, a shopping list two people can tick offline without losing an item, and keeping the whole box portable enough to leave. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A box that opens in a kitchen Recipes are structured, editable and readable on a phone with no signal. ### Steps 1. Make ingredients structured from the first migration Everything downstream depends on quantity and unit existing. Files: `src/db/schema.ts`, `src/lib/ingredients.ts` 2. Cache recipes for offline reading and say so A stale copy you know about is fine; one you do not is not. Files: `src/lib/offline.ts` 3. Scale quantities, refuse to scale time Doubling a cake does not double the bake. Files: `src/lib/scale.ts`, `tests/scale.test.ts` ### Done when - [ ] A recipe reads fully offline and the cache is disclosed - [ ] Quantity, unit and item are separate fields - [ ] Scaling touches quantities only, with a note - [ ] Oversized photos are rejected with a message ### Watch out - Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. ## Phase 2 · Cooking mode, and clocks you can trust The screen stays on when it can, and every timer is correct when you come back to it. ### Steps 1. Store end times, never remaining seconds The clock is the only thing that keeps running. Files: `src/lib/timers.ts`, `tests/timers.test.ts` 2. Re-request the wake lock on visibility change The lock is dropped when the page goes away and does not come back by itself. Files: `src/lib/wakelock.ts` 3. Say plainly that the alarm is best effort One honest sentence beats a silent failure over a hot pan. Files: `src/app/cook/page.tsx` ### Done when - [ ] A backgrounded timer reports finished with elapsed time - [ ] The wake lock is re-requested on becoming visible - [ ] A refused lock leaves cooking usable with a notice - [ ] A reload resumes the cook at the right step ### Watch out - Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. ## Phase 3 · A list two people can tick at once Two members editing the list offline both keep their changes. ### Steps 1. Merge per item, never per list This is the difference between a household tool and an argument. Files: `src/lib/list-sync.ts`, `tests/list-merge.test.ts` 2. Make deletions tombstones A stale device will otherwise bring the milk back. Files: `src/lib/list.ts` 3. Record which recipe contributed each line So removing a meal from the plan can remove its items. Files: `src/lib/list.ts` ### Done when - [ ] Offline edits to different items all survive - [ ] Same-item conflicts resolve to the later state change - [ ] Deleted items do not come back from a stale device - [ ] Merged lines record their contributing recipes ### Watch out - Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. ## Not in this build - Scraping recipes from other people's sites. - Retailer ordering and barcode product data. - Smart home and appliance integrations. - Nutrition figures. ## After v1, if you want it - A weekly plan that feeds the list and can be undone as one action when the week changes - A printable list grouped by shop aisle, which is the feature people actually use in a supermarket - A pantry with rough quantities, which only becomes useful once the list merge has been trusted for a while ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recipes, lists, cooks and timers. Back this up; recipes are typed once. DATABASE_PATH=./data/household.db # Required. Recipe photos. Included in every export as files. IMAGE_DIR=./data/images # Required. Used in invite links and needed for a secure origin. PUBLIC_BASE_URL=https://kitchen.local # Required. How long a household invite code stays valid. INVITE_CODE_TTL_HOURS=48 # Required. Per photo, after the device resizes it. MAX_IMAGE_BYTES=2097152 # Required. Scheduled archives of the database and images together. BACKUP_DIR=./backups
You are building a production product version of Pestle. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Pestle · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Pestle, save recipes, guide cooking steps, plan meals, and share a household list. The hard boundary is native apple polish, recipe import, hands-free mode, sync, and family sharing, plus sync, integrations, and household adoption. ## Product outcome A recipe box for one household where a recipe opens with no signal, a timer that ran while the phone was locked tells you the truth when you pick it up, two people can tick the same list from different places, and the whole thing exports as files you can read without this application. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - modern browser - local or self-hosted storage - optional email notifications ## Explicit non-goals for v1 - Scraping recipes from other people's sites. - Retailer ordering and barcode product data. - Smart home and appliance integrations. - Nutrition figures. - native Apple polish, recipe import, hands-free mode, sync, and family sharing - frictionless family sync - retailer and smart-home integrations - large recipe or product database - mobile polish ## Success criteria - A backgrounded timer is confirmed to report finished with elapsed time - A refused wake lock is confirmed to leave cooking usable - Offline edits to different list items are confirmed all kept - A deleted list item is confirmed not to return from a stale device - Export and import are confirmed to reproduce counts and image checksums ===== BRIEF.md ===== # Build brief · Pestle The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a household recipe box with a cooking mode whose timers are still right when you come back to the phone to replace Pestle. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The cooking screen is the part people judge this on, and the browser is actively unhelpful there. Two published behaviours decide the design. A screen wake lock is released automatically when the document stops being active, so the documented pattern is to listen for the visibility change event and request a new lock when the page becomes visible again. Support is recent and the platform can refuse a lock outright, on low battery or in a power saving mode, so the code has to work when the request fails. And background timers are throttled: a page that has been invisible for more than five minutes and silent for more than thirty seconds has its timers checked about once a minute in one major browser, another gives inactive desktop tabs a one second floor and inactive tabs on mobile a fifteen minute floor and may unload them entirely. Check the current numbers yourself. So a timer here is never a countdown living in a callback. It is an end time stored in the database. The display computes the remaining time from the clock on every render, and on load and on every return to visibility the app reconciles: anything whose end time has passed is shown as finished, with how long ago. The alarm is a best effort and the interface says so in one sentence, because a tool that silently fails to ring while somebody's stock reduces to tar has done harm. The second rule is smaller: this is a household, so two people will edit the same list at the same time and one of them will be in a supermarket with no signal. Never resolve that by taking the most recent whole list. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, installed to the home screen as a local-first web app · the kitchen is where connectivity is worst and a recipe must open anyway - Storage: SQLite on a small server for the household, with a local copy in the browser · one shared box, but every device has to work on its own for an hour - Timers: Stored end timestamps, remaining time computed from the clock on every render · background timers are throttled and a countdown in a callback is simply wrong - List sync: Per item state with last-writer-wins on the item, never on the list · two people shopping is the normal case and a whole-list overwrite loses food - Portability: Whole-box export as JSON plus images, importable into an empty instance · a recipe box you cannot take with you is a rental ### Data model (create this before Phase 1) - recipes(id, title, source_url, source_name, servings, notes, created_by, updated_at) - ingredients(recipe_id, position, quantity, unit, item, note) · structured rather than a free text line, because the list depends on it - steps(recipe_id, position, text, timer_seconds) · a step may carry a suggested duration that becomes a timer - cooks(id, recipe_id, started_at, current_step, finished_at) · one run through a recipe, so you can leave and come back - timers(id, cook_id, label, ends_at, acknowledged_at, cancelled_at) · an end time, never a remaining count - list_items(id, household_id, text, quantity, unit, source_recipe_id, state, state_changed_at, changed_by) · state is needed, in trolley or done - members(id, household_id, name, joined_at) and households(id, name, invite_code, created_at) ### Phase 1 · A box that opens in a kitchen Build: Recipes with structured ingredient rows and ordered steps, entered by hand or pasted and split. Cache everything needed to read a recipe locally so it opens offline, and show clearly when you are looking at a cached copy. Photos optional, resized on the device, with a size limit. Scaling adjusts quantities and says plainly that it does not adjust times or temperatures. Trap: Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. Done when: A recipe opens and is fully readable with the network disabled, tested in the browser's offline mode, and the cached state is shown. Ingredients are stored as quantity, unit and item in separate fields, verified in the database. Scaling changes quantities only and displays a note that times and temperatures are unchanged. A photo over the size limit is rejected with a message rather than silently resized to nothing. Do not build yet: No cooking mode, list, household or export yet. ### Phase 2 · Cooking mode, and clocks you can trust Build: A step-at-a-time view with large text and one thumb-sized control. Request a screen wake lock on entry and re-request it whenever the page becomes visible again. Timers store an end time; the display derives the remaining time from the clock. On load and on every return to visibility, reconcile every timer and show finished ones with how long ago they finished. Trap: Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. Done when: A timer started, then backgrounded past its end, shows as finished with the elapsed time on return, tested by moving the clock forward with the page hidden. The wake lock is re-requested on the visibility change event and a refused lock leaves the cooking view fully usable with a visible notice, tested by forcing rejection. No timer state is held only in memory, verified by reloading mid-cook. A cook in progress resumes at the right step after a reload. Do not build yet: No shopping list, household or export yet. ### Phase 3 · A list two people can tick at once Build: A shopping list of items with a state each, changed independently. Queue changes made offline and replay them on reconnect, merging per item by the time the state changed rather than by the time the list was written. Adding a recipe to the list merges ingredients by item and unit, and keeps a note of which recipes contributed. Deletions are tombstones, not row removals. Trap: Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. Done when: Two devices editing different items offline both keep their changes after reconnecting, tested with a simulated partition. Two devices changing the same item resolve to the later state change and the earlier one is visible in history. A deleted item does not reappear when a device that was offline reconnects, tested. Adding a recipe merges by item and unit and records which recipes contributed each line. Do not build yet: No household management, backup or export yet. ### Phase 4 · A household, and the door out Build: Join by an invite code, with every change attributed to a member and undoable. Removing a member keeps their recipes and stops their access. Export the whole household as JSON plus image files, and import it into an empty instance. Back up on a schedule with a restore exercised in tests. Trap: Exporting recipes as JSON while leaving the photos on the server means the export looks complete and is missing the only part nobody can retype, and people discover this when the server is already gone. Export images as files alongside the JSON, reference them by relative path, and test the import into an empty instance rather than assuming it. Done when: An export contains every recipe, list item and image file and imports into an empty instance with matching counts and checksums, exercised in the test suite. Removing a member revokes access immediately and leaves their contributions with their name intact. Every change is attributed to a member and can be undone from a history view. A restore from a scheduled backup reproduces the household exactly. Do not build yet: Nothing after this. ### Out of scope (and why) - Importing recipes by scraping other people's sites, which is a separate project with its own permissions and copyright questions and does not belong in a first build. - Retailer ordering and barcode product data, which is a commercial data licence rather than code. - Smart home and appliance integrations, which are a different API per manufacturer and a maintenance burden forever. - Nutrition figures, because a number computed from a database of ingredient averages and presented beside a recipe reads as measurement when it is estimate. ### README must contain - That the cooking alarm is best effort, because browsers throttle background timers, and what the current published throttling behaviour is - That the screen wake lock is released when the page is hidden and re-requested on return, and that a platform may refuse it - That every timer is stored as an end time and reconciled on load, so a backgrounded timer is never wrong - That the shopping list merges per item, and what happens when two members change the same item - How to export the whole household including images, and how to import it into a fresh instance ===== ARCHITECTURE.md ===== # Architecture · Pestle ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, installed to the home screen as a local-first web app | the kitchen is where connectivity is worst and a recipe must open anyway | | Storage | SQLite on a small server for the household, with a local copy in the browser | one shared box, but every device has to work on its own for an hour | | Timers | Stored end timestamps, remaining time computed from the clock on every render | background timers are throttled and a countdown in a callback is simply wrong | | List sync | Per item state with last-writer-wins on the item, never on the list | two people shopping is the normal case and a whole-list overwrite loses food | | Portability | Whole-box export as JSON plus images, importable into an empty instance | a recipe box you cannot take with you is a rental | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Recipes | Structured ingredients, steps, scaling. | Any store, provided quantity and unit stay separate fields. | | Cooking | Step view, wake lock, timers. | Any timer implementation, provided it stores end times rather than counts. | | List | Per item state, offline queue, merges. | Any sync, provided it merges per item and keeps tombstones. | | Household | Members, invites, attribution. | Any scheme, provided removal revokes without erasing. | | Portability | Export, import, backup, restore. | Any format, provided images travel with the data. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Recipes, lists, cooks and timers. Back this up; recipes are typed once. - `IMAGE_DIR` · required · Recipe photos. Included in every export as files. - `PUBLIC_BASE_URL` · required · Used in invite links and needed for a secure origin. - `INVITE_CODE_TTL_HOURS` · required · How long a household invite code stays valid. - `MAX_IMAGE_BYTES` · required · Per photo, after the device resizes it. - `BACKUP_DIR` · required · Scheduled archives of the database and images together. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Pestle product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js with TypeScript, installed to the home screen as a local-first web app, SQLite on a small server for the household, with a local copy in the browser, Stored end timestamps, remaining time computed from the clock on every render, Per item state with last-writer-wins on the item, never on the list, Whole-box export as JSON plus images, importable into an empty instance. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. - Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. - Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. - Exporting recipes as JSON while leaving the photos on the server means the export looks complete and is missing the only part nobody can retype, and people discover this when the server is already gone. Export images as files alongside the JSON, reference them by relative path, and test the import into an empty instance rather than assuming it. ===== MILESTONES.md ===== # Delivery milestones · Pestle Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A box that opens in a kitchen Recipes are structured, editable and readable on a phone with no signal. ### Steps 1. Make ingredients structured from the first migration Everything downstream depends on quantity and unit existing. Files: `src/db/schema.ts`, `src/lib/ingredients.ts` 2. Cache recipes for offline reading and say so A stale copy you know about is fine; one you do not is not. Files: `src/lib/offline.ts` 3. Scale quantities, refuse to scale time Doubling a cake does not double the bake. Files: `src/lib/scale.ts`, `tests/scale.test.ts` ### Done when - [ ] A recipe reads fully offline and the cache is disclosed - [ ] Quantity, unit and item are separate fields - [ ] Scaling touches quantities only, with a note - [ ] Oversized photos are rejected with a message ### Watch out - Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. ## M2 · Cooking mode, and clocks you can trust The screen stays on when it can, and every timer is correct when you come back to it. ### Steps 1. Store end times, never remaining seconds The clock is the only thing that keeps running. Files: `src/lib/timers.ts`, `tests/timers.test.ts` 2. Re-request the wake lock on visibility change The lock is dropped when the page goes away and does not come back by itself. Files: `src/lib/wakelock.ts` 3. Say plainly that the alarm is best effort One honest sentence beats a silent failure over a hot pan. Files: `src/app/cook/page.tsx` ### Done when - [ ] A backgrounded timer reports finished with elapsed time - [ ] The wake lock is re-requested on becoming visible - [ ] A refused lock leaves cooking usable with a notice - [ ] A reload resumes the cook at the right step ### Watch out - Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. ## M3 · A list two people can tick at once Two members editing the list offline both keep their changes. ### Steps 1. Merge per item, never per list This is the difference between a household tool and an argument. Files: `src/lib/list-sync.ts`, `tests/list-merge.test.ts` 2. Make deletions tombstones A stale device will otherwise bring the milk back. Files: `src/lib/list.ts` 3. Record which recipe contributed each line So removing a meal from the plan can remove its items. Files: `src/lib/list.ts` ### Done when - [ ] Offline edits to different items all survive - [ ] Same-item conflicts resolve to the later state change - [ ] Deleted items do not come back from a stale device - [ ] Merged lines record their contributing recipes ### Watch out - Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. ## M4 · A household, and the door out (production only) People can join and leave cleanly, and the whole box can be exported and reimported. ### Steps 1. Export images as files beside the JSON They are the part nobody can recreate. Files: `scripts/export.ts`, `tests/export.test.ts` 2. Make removal revoke without erasing People leave households; their recipes should not. Files: `src/lib/members.ts` 3. Exercise the restore in the suite The first restore should not be the one that matters. Files: `tests/restore.test.ts` ### Done when - [ ] Export plus import reproduces counts and checksums - [ ] Removing a member revokes access and keeps their work - [ ] Changes are attributed and undoable - [ ] A scheduled backup restores exactly ### Watch out - Exporting recipes as JSON while leaving the photos on the server means the export looks complete and is missing the only part nobody can retype, and people discover this when the server is already gone. Export images as files alongside the JSON, reference them by relative path, and test the import into an empty instance rather than assuming it. ===== OPERATIONS.md ===== # Operations · Pestle ## Backup The database and the image directory together, on a schedule, off the household machine. Recipes are typed in by hand over years and photos are taken once, so this is the one irreplaceable pile in the house. ## Restore Restore both together into an empty instance and compare recipe, list and image counts plus image checksums. A restore that brings back the database but not the images leaves every recipe looking broken to the people who use it most. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Backup age, disk used by images, failed sync replays sitting in the offline queue, and the count of timers that finished unacknowledged. A growing unacknowledged count means the alarm is not reaching anyone. ## Incident checklist The incidents are a lost recipe box and a shopping list that quietly dropped items. The first is answered by the rehearsed restore, the second by the per item merge test, and both should fail the build rather than be noticed at dinner. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A backgrounded timer is confirmed to report finished with elapsed time - [ ] A refused wake lock is confirmed to leave cooking usable - [ ] Offline edits to different list items are confirmed all kept - [ ] A deleted list item is confirmed not to return from a stale device - [ ] Export and import are confirmed to reproduce counts and image checksums ## Launch constraint Do not market omitted Pestle capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recipes, lists, cooks and timers. Back this up; recipes are typed once. DATABASE_PATH=./data/household.db # Required. Recipe photos. Included in every export as files. IMAGE_DIR=./data/images # Required. Used in invite links and needed for a secure origin. PUBLIC_BASE_URL=https://kitchen.local # Required. How long a household invite code stays valid. INVITE_CODE_TTL_HOURS=48 # Required. Per photo, after the device resizes it. MAX_IMAGE_BYTES=2097152 # Required. Scheduled archives of the database and images together. BACKUP_DIR=./backups
# Pestle · indie build A household recipe box whose hard problems are a cooking mode that keeps working when the browser throttles it, a shopping list two people can tick offline without losing an item, and keeping the whole box portable enough to leave. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, installed to the home screen as a local-first web app | the kitchen is where connectivity is worst and a recipe must open anyway | | Storage | SQLite on a small server for the household, with a local copy in the browser | one shared box, but every device has to work on its own for an hour | | Timers | Stored end timestamps, remaining time computed from the clock on every render | background timers are throttled and a countdown in a callback is simply wrong | | List sync | Per item state with last-writer-wins on the item, never on the list | two people shopping is the normal case and a whole-list overwrite loses food | | Portability | Whole-box export as JSON plus images, importable into an empty instance | a recipe box you cannot take with you is a rental | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the household server and the build. - Get it: nodejs.org or your version manager. - Verify: node --version prints v22 or higher - [ ] **A phone with the app installed to the home screen** · free - Why: Cooking mode, the wake lock and the timer reconciliation can only be judged on the device that will be on a worktop with wet hands. - Get it: Open the app on the phone over the local network and add it to the home screen. - Verify: A timer started and backgrounded reports correctly on return - [ ] **A small always-on machine on your network, with a certificate** · free on hardware you own, or a few pounds a month - Why: The household needs one place to agree with, and the wake lock and installability both want a secure origin. - Get it: Any small machine or home server with a local certificate, or a cheap instance with a real one. - Verify: Two devices reach the same household over a valid certificate - [ ] **A second device you can take offline** · free - Why: The list merge is the part most likely to lose data and it cannot be tested with one device. - Get it: Any second phone, tablet or browser profile that can be put into aeroplane mode. - Verify: Both devices edit offline and reconcile without loss ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Scraping recipes from other people's sites. - Retailer ordering and barcode product data. - Smart home and appliance integrations. - Nutrition figures. - native Apple polish, recipe import, hands-free mode, sync, and family sharing - frictionless family sync - retailer and smart-home integrations - large recipe or product database - mobile polish If one of those is essential to you, that is the reason to keep paying for Pestle, and the README should say so rather than pretend.
# Build brief · Pestle The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a household recipe box with a cooking mode whose timers are still right when you come back to the phone to replace Pestle. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The cooking screen is the part people judge this on, and the browser is actively unhelpful there. Two published behaviours decide the design. A screen wake lock is released automatically when the document stops being active, so the documented pattern is to listen for the visibility change event and request a new lock when the page becomes visible again. Support is recent and the platform can refuse a lock outright, on low battery or in a power saving mode, so the code has to work when the request fails. And background timers are throttled: a page that has been invisible for more than five minutes and silent for more than thirty seconds has its timers checked about once a minute in one major browser, another gives inactive desktop tabs a one second floor and inactive tabs on mobile a fifteen minute floor and may unload them entirely. Check the current numbers yourself. So a timer here is never a countdown living in a callback. It is an end time stored in the database. The display computes the remaining time from the clock on every render, and on load and on every return to visibility the app reconciles: anything whose end time has passed is shown as finished, with how long ago. The alarm is a best effort and the interface says so in one sentence, because a tool that silently fails to ring while somebody's stock reduces to tar has done harm. The second rule is smaller: this is a household, so two people will edit the same list at the same time and one of them will be in a supermarket with no signal. Never resolve that by taking the most recent whole list. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, installed to the home screen as a local-first web app · the kitchen is where connectivity is worst and a recipe must open anyway - Storage: SQLite on a small server for the household, with a local copy in the browser · one shared box, but every device has to work on its own for an hour - Timers: Stored end timestamps, remaining time computed from the clock on every render · background timers are throttled and a countdown in a callback is simply wrong - List sync: Per item state with last-writer-wins on the item, never on the list · two people shopping is the normal case and a whole-list overwrite loses food - Portability: Whole-box export as JSON plus images, importable into an empty instance · a recipe box you cannot take with you is a rental ### Data model (create this before Phase 1) - recipes(id, title, source_url, source_name, servings, notes, created_by, updated_at) - ingredients(recipe_id, position, quantity, unit, item, note) · structured rather than a free text line, because the list depends on it - steps(recipe_id, position, text, timer_seconds) · a step may carry a suggested duration that becomes a timer - cooks(id, recipe_id, started_at, current_step, finished_at) · one run through a recipe, so you can leave and come back - timers(id, cook_id, label, ends_at, acknowledged_at, cancelled_at) · an end time, never a remaining count - list_items(id, household_id, text, quantity, unit, source_recipe_id, state, state_changed_at, changed_by) · state is needed, in trolley or done - members(id, household_id, name, joined_at) and households(id, name, invite_code, created_at) ### Phase 1 · A box that opens in a kitchen Build: Recipes with structured ingredient rows and ordered steps, entered by hand or pasted and split. Cache everything needed to read a recipe locally so it opens offline, and show clearly when you are looking at a cached copy. Photos optional, resized on the device, with a size limit. Scaling adjusts quantities and says plainly that it does not adjust times or temperatures. Trap: Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. Done when: A recipe opens and is fully readable with the network disabled, tested in the browser's offline mode, and the cached state is shown. Ingredients are stored as quantity, unit and item in separate fields, verified in the database. Scaling changes quantities only and displays a note that times and temperatures are unchanged. A photo over the size limit is rejected with a message rather than silently resized to nothing. Do not build yet: No cooking mode, list, household or export yet. ### Phase 2 · Cooking mode, and clocks you can trust Build: A step-at-a-time view with large text and one thumb-sized control. Request a screen wake lock on entry and re-request it whenever the page becomes visible again. Timers store an end time; the display derives the remaining time from the clock. On load and on every return to visibility, reconcile every timer and show finished ones with how long ago they finished. Trap: Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. Done when: A timer started, then backgrounded past its end, shows as finished with the elapsed time on return, tested by moving the clock forward with the page hidden. The wake lock is re-requested on the visibility change event and a refused lock leaves the cooking view fully usable with a visible notice, tested by forcing rejection. No timer state is held only in memory, verified by reloading mid-cook. A cook in progress resumes at the right step after a reload. Do not build yet: No shopping list, household or export yet. ### Phase 3 · A list two people can tick at once Build: A shopping list of items with a state each, changed independently. Queue changes made offline and replay them on reconnect, merging per item by the time the state changed rather than by the time the list was written. Adding a recipe to the list merges ingredients by item and unit, and keeps a note of which recipes contributed. Deletions are tombstones, not row removals. Trap: Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. Done when: Two devices editing different items offline both keep their changes after reconnecting, tested with a simulated partition. Two devices changing the same item resolve to the later state change and the earlier one is visible in history. A deleted item does not reappear when a device that was offline reconnects, tested. Adding a recipe merges by item and unit and records which recipes contributed each line. Do not build yet: No household management, backup or export yet. ### Phase 4 · A household, and the door out Build: Join by an invite code, with every change attributed to a member and undoable. Removing a member keeps their recipes and stops their access. Export the whole household as JSON plus image files, and import it into an empty instance. Back up on a schedule with a restore exercised in tests. Trap: Exporting recipes as JSON while leaving the photos on the server means the export looks complete and is missing the only part nobody can retype, and people discover this when the server is already gone. Export images as files alongside the JSON, reference them by relative path, and test the import into an empty instance rather than assuming it. Done when: An export contains every recipe, list item and image file and imports into an empty instance with matching counts and checksums, exercised in the test suite. Removing a member revokes access immediately and leaves their contributions with their name intact. Every change is attributed to a member and can be undone from a history view. A restore from a scheduled backup reproduces the household exactly. Do not build yet: Nothing after this. ### Out of scope (and why) - Importing recipes by scraping other people's sites, which is a separate project with its own permissions and copyright questions and does not belong in a first build. - Retailer ordering and barcode product data, which is a commercial data licence rather than code. - Smart home and appliance integrations, which are a different API per manufacturer and a maintenance burden forever. - Nutrition figures, because a number computed from a database of ingredient averages and presented beside a recipe reads as measurement when it is estimate. ### README must contain - That the cooking alarm is best effort, because browsers throttle background timers, and what the current published throttling behaviour is - That the screen wake lock is released when the page is hidden and re-requested on return, and that a platform may refuse it - That every timer is stored as an end time and reconciled on load, so a backgrounded timer is never wrong - That the shopping list merges per item, and what happens when two members change the same item - How to export the whole household including images, and how to import it into a fresh instance
# Agent instructions · Pestle indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, installed to the home screen as a local-first web app, SQLite on a small server for the household, with a local copy in the browser, Stored end timestamps, remaining time computed from the clock on every render, Per item state with last-writer-wins on the item, never on the list, Whole-box export as JSON plus images, importable into an empty instance. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. - Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. - Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them.
# Build plan · Pestle A household recipe box whose hard problems are a cooking mode that keeps working when the browser throttles it, a shopping list two people can tick offline without losing an item, and keeping the whole box portable enough to leave. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A box that opens in a kitchen Recipes are structured, editable and readable on a phone with no signal. ### Steps 1. Make ingredients structured from the first migration Everything downstream depends on quantity and unit existing. Files: `src/db/schema.ts`, `src/lib/ingredients.ts` 2. Cache recipes for offline reading and say so A stale copy you know about is fine; one you do not is not. Files: `src/lib/offline.ts` 3. Scale quantities, refuse to scale time Doubling a cake does not double the bake. Files: `src/lib/scale.ts`, `tests/scale.test.ts` ### Done when - [ ] A recipe reads fully offline and the cache is disclosed - [ ] Quantity, unit and item are separate fields - [ ] Scaling touches quantities only, with a note - [ ] Oversized photos are rejected with a message ### Watch out - Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. ## Phase 2 · Cooking mode, and clocks you can trust The screen stays on when it can, and every timer is correct when you come back to it. ### Steps 1. Store end times, never remaining seconds The clock is the only thing that keeps running. Files: `src/lib/timers.ts`, `tests/timers.test.ts` 2. Re-request the wake lock on visibility change The lock is dropped when the page goes away and does not come back by itself. Files: `src/lib/wakelock.ts` 3. Say plainly that the alarm is best effort One honest sentence beats a silent failure over a hot pan. Files: `src/app/cook/page.tsx` ### Done when - [ ] A backgrounded timer reports finished with elapsed time - [ ] The wake lock is re-requested on becoming visible - [ ] A refused lock leaves cooking usable with a notice - [ ] A reload resumes the cook at the right step ### Watch out - Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. ## Phase 3 · A list two people can tick at once Two members editing the list offline both keep their changes. ### Steps 1. Merge per item, never per list This is the difference between a household tool and an argument. Files: `src/lib/list-sync.ts`, `tests/list-merge.test.ts` 2. Make deletions tombstones A stale device will otherwise bring the milk back. Files: `src/lib/list.ts` 3. Record which recipe contributed each line So removing a meal from the plan can remove its items. Files: `src/lib/list.ts` ### Done when - [ ] Offline edits to different items all survive - [ ] Same-item conflicts resolve to the later state change - [ ] Deleted items do not come back from a stale device - [ ] Merged lines record their contributing recipes ### Watch out - Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. ## Not in this build - Scraping recipes from other people's sites. - Retailer ordering and barcode product data. - Smart home and appliance integrations. - Nutrition figures. ## After v1, if you want it - A weekly plan that feeds the list and can be undone as one action when the week changes - A printable list grouped by shop aisle, which is the feature people actually use in a supermarket - A pantry with rough quantities, which only becomes useful once the list merge has been trusted for a while
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recipes, lists, cooks and timers. Back this up; recipes are typed once. DATABASE_PATH=./data/household.db # Required. Recipe photos. Included in every export as files. IMAGE_DIR=./data/images # Required. Used in invite links and needed for a secure origin. PUBLIC_BASE_URL=https://kitchen.local # Required. How long a household invite code stays valid. INVITE_CODE_TTL_HOURS=48 # Required. Per photo, after the device resizes it. MAX_IMAGE_BYTES=2097152 # Required. Scheduled archives of the database and images together. BACKUP_DIR=./backups
# Pestle · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Pestle, save recipes, guide cooking steps, plan meals, and share a household list. The hard boundary is native apple polish, recipe import, hands-free mode, sync, and family sharing, plus sync, integrations, and household adoption. ## Product outcome A recipe box for one household where a recipe opens with no signal, a timer that ran while the phone was locked tells you the truth when you pick it up, two people can tick the same list from different places, and the whole thing exports as files you can read without this application. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - modern browser - local or self-hosted storage - optional email notifications ## Explicit non-goals for v1 - Scraping recipes from other people's sites. - Retailer ordering and barcode product data. - Smart home and appliance integrations. - Nutrition figures. - native Apple polish, recipe import, hands-free mode, sync, and family sharing - frictionless family sync - retailer and smart-home integrations - large recipe or product database - mobile polish ## Success criteria - A backgrounded timer is confirmed to report finished with elapsed time - A refused wake lock is confirmed to leave cooking usable - Offline edits to different list items are confirmed all kept - A deleted list item is confirmed not to return from a stale device - Export and import are confirmed to reproduce counts and image checksums
# Build brief · Pestle The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a household recipe box with a cooking mode whose timers are still right when you come back to the phone to replace Pestle. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The cooking screen is the part people judge this on, and the browser is actively unhelpful there. Two published behaviours decide the design. A screen wake lock is released automatically when the document stops being active, so the documented pattern is to listen for the visibility change event and request a new lock when the page becomes visible again. Support is recent and the platform can refuse a lock outright, on low battery or in a power saving mode, so the code has to work when the request fails. And background timers are throttled: a page that has been invisible for more than five minutes and silent for more than thirty seconds has its timers checked about once a minute in one major browser, another gives inactive desktop tabs a one second floor and inactive tabs on mobile a fifteen minute floor and may unload them entirely. Check the current numbers yourself. So a timer here is never a countdown living in a callback. It is an end time stored in the database. The display computes the remaining time from the clock on every render, and on load and on every return to visibility the app reconciles: anything whose end time has passed is shown as finished, with how long ago. The alarm is a best effort and the interface says so in one sentence, because a tool that silently fails to ring while somebody's stock reduces to tar has done harm. The second rule is smaller: this is a household, so two people will edit the same list at the same time and one of them will be in a supermarket with no signal. Never resolve that by taking the most recent whole list. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, installed to the home screen as a local-first web app · the kitchen is where connectivity is worst and a recipe must open anyway - Storage: SQLite on a small server for the household, with a local copy in the browser · one shared box, but every device has to work on its own for an hour - Timers: Stored end timestamps, remaining time computed from the clock on every render · background timers are throttled and a countdown in a callback is simply wrong - List sync: Per item state with last-writer-wins on the item, never on the list · two people shopping is the normal case and a whole-list overwrite loses food - Portability: Whole-box export as JSON plus images, importable into an empty instance · a recipe box you cannot take with you is a rental ### Data model (create this before Phase 1) - recipes(id, title, source_url, source_name, servings, notes, created_by, updated_at) - ingredients(recipe_id, position, quantity, unit, item, note) · structured rather than a free text line, because the list depends on it - steps(recipe_id, position, text, timer_seconds) · a step may carry a suggested duration that becomes a timer - cooks(id, recipe_id, started_at, current_step, finished_at) · one run through a recipe, so you can leave and come back - timers(id, cook_id, label, ends_at, acknowledged_at, cancelled_at) · an end time, never a remaining count - list_items(id, household_id, text, quantity, unit, source_recipe_id, state, state_changed_at, changed_by) · state is needed, in trolley or done - members(id, household_id, name, joined_at) and households(id, name, invite_code, created_at) ### Phase 1 · A box that opens in a kitchen Build: Recipes with structured ingredient rows and ordered steps, entered by hand or pasted and split. Cache everything needed to read a recipe locally so it opens offline, and show clearly when you are looking at a cached copy. Photos optional, resized on the device, with a size limit. Scaling adjusts quantities and says plainly that it does not adjust times or temperatures. Trap: Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. Done when: A recipe opens and is fully readable with the network disabled, tested in the browser's offline mode, and the cached state is shown. Ingredients are stored as quantity, unit and item in separate fields, verified in the database. Scaling changes quantities only and displays a note that times and temperatures are unchanged. A photo over the size limit is rejected with a message rather than silently resized to nothing. Do not build yet: No cooking mode, list, household or export yet. ### Phase 2 · Cooking mode, and clocks you can trust Build: A step-at-a-time view with large text and one thumb-sized control. Request a screen wake lock on entry and re-request it whenever the page becomes visible again. Timers store an end time; the display derives the remaining time from the clock. On load and on every return to visibility, reconcile every timer and show finished ones with how long ago they finished. Trap: Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. Done when: A timer started, then backgrounded past its end, shows as finished with the elapsed time on return, tested by moving the clock forward with the page hidden. The wake lock is re-requested on the visibility change event and a refused lock leaves the cooking view fully usable with a visible notice, tested by forcing rejection. No timer state is held only in memory, verified by reloading mid-cook. A cook in progress resumes at the right step after a reload. Do not build yet: No shopping list, household or export yet. ### Phase 3 · A list two people can tick at once Build: A shopping list of items with a state each, changed independently. Queue changes made offline and replay them on reconnect, merging per item by the time the state changed rather than by the time the list was written. Adding a recipe to the list merges ingredients by item and unit, and keeps a note of which recipes contributed. Deletions are tombstones, not row removals. Trap: Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. Done when: Two devices editing different items offline both keep their changes after reconnecting, tested with a simulated partition. Two devices changing the same item resolve to the later state change and the earlier one is visible in history. A deleted item does not reappear when a device that was offline reconnects, tested. Adding a recipe merges by item and unit and records which recipes contributed each line. Do not build yet: No household management, backup or export yet. ### Phase 4 · A household, and the door out Build: Join by an invite code, with every change attributed to a member and undoable. Removing a member keeps their recipes and stops their access. Export the whole household as JSON plus image files, and import it into an empty instance. Back up on a schedule with a restore exercised in tests. Trap: Exporting recipes as JSON while leaving the photos on the server means the export looks complete and is missing the only part nobody can retype, and people discover this when the server is already gone. Export images as files alongside the JSON, reference them by relative path, and test the import into an empty instance rather than assuming it. Done when: An export contains every recipe, list item and image file and imports into an empty instance with matching counts and checksums, exercised in the test suite. Removing a member revokes access immediately and leaves their contributions with their name intact. Every change is attributed to a member and can be undone from a history view. A restore from a scheduled backup reproduces the household exactly. Do not build yet: Nothing after this. ### Out of scope (and why) - Importing recipes by scraping other people's sites, which is a separate project with its own permissions and copyright questions and does not belong in a first build. - Retailer ordering and barcode product data, which is a commercial data licence rather than code. - Smart home and appliance integrations, which are a different API per manufacturer and a maintenance burden forever. - Nutrition figures, because a number computed from a database of ingredient averages and presented beside a recipe reads as measurement when it is estimate. ### README must contain - That the cooking alarm is best effort, because browsers throttle background timers, and what the current published throttling behaviour is - That the screen wake lock is released when the page is hidden and re-requested on return, and that a platform may refuse it - That every timer is stored as an end time and reconciled on load, so a backgrounded timer is never wrong - That the shopping list merges per item, and what happens when two members change the same item - How to export the whole household including images, and how to import it into a fresh instance
# Architecture · Pestle ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, installed to the home screen as a local-first web app | the kitchen is where connectivity is worst and a recipe must open anyway | | Storage | SQLite on a small server for the household, with a local copy in the browser | one shared box, but every device has to work on its own for an hour | | Timers | Stored end timestamps, remaining time computed from the clock on every render | background timers are throttled and a countdown in a callback is simply wrong | | List sync | Per item state with last-writer-wins on the item, never on the list | two people shopping is the normal case and a whole-list overwrite loses food | | Portability | Whole-box export as JSON plus images, importable into an empty instance | a recipe box you cannot take with you is a rental | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Recipes | Structured ingredients, steps, scaling. | Any store, provided quantity and unit stay separate fields. | | Cooking | Step view, wake lock, timers. | Any timer implementation, provided it stores end times rather than counts. | | List | Per item state, offline queue, merges. | Any sync, provided it merges per item and keeps tombstones. | | Household | Members, invites, attribution. | Any scheme, provided removal revokes without erasing. | | Portability | Export, import, backup, restore. | Any format, provided images travel with the data. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Recipes, lists, cooks and timers. Back this up; recipes are typed once. - `IMAGE_DIR` · required · Recipe photos. Included in every export as files. - `PUBLIC_BASE_URL` · required · Used in invite links and needed for a secure origin. - `INVITE_CODE_TTL_HOURS` · required · How long a household invite code stays valid. - `MAX_IMAGE_BYTES` · required · Per photo, after the device resizes it. - `BACKUP_DIR` · required · Scheduled archives of the database and images together. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Pestle product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js with TypeScript, installed to the home screen as a local-first web app, SQLite on a small server for the household, with a local copy in the browser, Stored end timestamps, remaining time computed from the clock on every render, Per item state with last-writer-wins on the item, never on the list, Whole-box export as JSON plus images, importable into an empty instance. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. - Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. - Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. - Exporting recipes as JSON while leaving the photos on the server means the export looks complete and is missing the only part nobody can retype, and people discover this when the server is already gone. Export images as files alongside the JSON, reference them by relative path, and test the import into an empty instance rather than assuming it.
# Delivery milestones · Pestle Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A box that opens in a kitchen Recipes are structured, editable and readable on a phone with no signal. ### Steps 1. Make ingredients structured from the first migration Everything downstream depends on quantity and unit existing. Files: `src/db/schema.ts`, `src/lib/ingredients.ts` 2. Cache recipes for offline reading and say so A stale copy you know about is fine; one you do not is not. Files: `src/lib/offline.ts` 3. Scale quantities, refuse to scale time Doubling a cake does not double the bake. Files: `src/lib/scale.ts`, `tests/scale.test.ts` ### Done when - [ ] A recipe reads fully offline and the cache is disclosed - [ ] Quantity, unit and item are separate fields - [ ] Scaling touches quantities only, with a note - [ ] Oversized photos are rejected with a message ### Watch out - Storing ingredients as one text line per row is quick and makes every later feature impossible, because a shopping list cannot merge two hundred grams of butter with a knob of butter when neither has a quantity or a unit. Store quantity, unit and item separately from the start and accept a fiddlier entry form. ## M2 · Cooking mode, and clocks you can trust The screen stays on when it can, and every timer is correct when you come back to it. ### Steps 1. Store end times, never remaining seconds The clock is the only thing that keeps running. Files: `src/lib/timers.ts`, `tests/timers.test.ts` 2. Re-request the wake lock on visibility change The lock is dropped when the page goes away and does not come back by itself. Files: `src/lib/wakelock.ts` 3. Say plainly that the alarm is best effort One honest sentence beats a silent failure over a hot pan. Files: `src/app/cook/page.tsx` ### Done when - [ ] A backgrounded timer reports finished with elapsed time - [ ] The wake lock is re-requested on becoming visible - [ ] A refused lock leaves cooking usable with a notice - [ ] A reload resumes the cook at the right step ### Watch out - Building the timer as a countdown in a repeating callback means that when the phone locks or the tab goes to the background the callback is throttled or stopped, and the user returns to a timer showing four minutes left that should have rung twenty minutes ago. Store the end time, compute from the clock, and reconcile on every wake. ## M3 · A list two people can tick at once Two members editing the list offline both keep their changes. ### Steps 1. Merge per item, never per list This is the difference between a household tool and an argument. Files: `src/lib/list-sync.ts`, `tests/list-merge.test.ts` 2. Make deletions tombstones A stale device will otherwise bring the milk back. Files: `src/lib/list.ts` 3. Record which recipe contributed each line So removing a meal from the plan can remove its items. Files: `src/lib/list.ts` ### Done when - [ ] Offline edits to different items all survive - [ ] Same-item conflicts resolve to the later state change - [ ] Deleted items do not come back from a stale device - [ ] Merged lines record their contributing recipes ### Watch out - Syncing the list as one document and taking whichever copy was saved most recently means the person in the shop who ticked off eight things overwrites the person at home who added three, and nobody finds out until dinner. Merge per item on the time its own state changed, and keep deletions as tombstones so a slow device cannot resurrect them. ## M4 · A household, and the door out (production only) People can join and leave cleanly, and the whole box can be exported and reimported. ### Steps 1. Export images as files beside the JSON They are the part nobody can recreate. Files: `scripts/export.ts`, `tests/export.test.ts` 2. Make removal revoke without erasing People leave households; their recipes should not. Files: `src/lib/members.ts` 3. Exercise the restore in the suite The first restore should not be the one that matters. Files: `tests/restore.test.ts` ### Done when - [ ] Export plus import reproduces counts and checksums - [ ] Removing a member revokes access and keeps their work - [ ] Changes are attributed and undoable - [ ] A scheduled backup restores exactly ### Watch out - Exporting recipes as JSON while leaving the photos on the server means the export looks complete and is missing the only part nobody can retype, and people discover this when the server is already gone. Export images as files alongside the JSON, reference them by relative path, and test the import into an empty instance rather than assuming it.
# Operations · Pestle ## Backup The database and the image directory together, on a schedule, off the household machine. Recipes are typed in by hand over years and photos are taken once, so this is the one irreplaceable pile in the house. ## Restore Restore both together into an empty instance and compare recipe, list and image counts plus image checksums. A restore that brings back the database but not the images leaves every recipe looking broken to the people who use it most. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Backup age, disk used by images, failed sync replays sitting in the offline queue, and the count of timers that finished unacknowledged. A growing unacknowledged count means the alarm is not reaching anyone. ## Incident checklist The incidents are a lost recipe box and a shopping list that quietly dropped items. The first is answered by the rehearsed restore, the second by the per item merge test, and both should fail the build rather than be noticed at dinner. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A backgrounded timer is confirmed to report finished with elapsed time - [ ] A refused wake lock is confirmed to leave cooking usable - [ ] Offline edits to different list items are confirmed all kept - [ ] A deleted list item is confirmed not to return from a stale device - [ ] Export and import are confirmed to reproduce counts and image checksums ## Launch constraint Do not market omitted Pestle capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recipes, lists, cooks and timers. Back this up; recipes are typed once. DATABASE_PATH=./data/household.db # Required. Recipe photos. Included in every export as files. IMAGE_DIR=./data/images # Required. Used in invite links and needed for a secure origin. PUBLIC_BASE_URL=https://kitchen.local # Required. How long a household invite code stays valid. INVITE_CODE_TTL_HOURS=48 # Required. Per photo, after the device resizes it. MAX_IMAGE_BYTES=2097152 # Required. Scheduled archives of the database and images together. BACKUP_DIR=./backups
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Pestle because household apps survive when every family member actually uses them and the data stays current across devices. The recurring cost buys multi-user conflicts, reminders, notifications, imports, barcode data, smart-home APIs, backups, and long-term maintenance, not just the visible interface.
xnative Apple polish, recipe import, hands-free mode, sync, and family sharing
xfrictionless family sync
xretailer and smart-home integrations
xlarge recipe or product database
xmobile polish
Pestle pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | Up to 15 recipes; includes website import, shopping lists, guided cooking, scaling, and unit conversion. |
| pestle pro | $2.99 | $2.08 | Unlimited recipes, device sync, household sharing, meal planning, and image/PDF recipe import. |
| pestle pro lifetime | custom | — | Same Pro feature set without recurring billing. |
free tier15 recipes; no published numeric caps for shopping lists or guided-cooking sessions.
billingmonthly + annual + lifetime; 14-day trial
hidden costsThe App Store lists more than one lifetime SKU ($39.99 and $49.99), so the price shown in-app can differ from the $39.99 official-shop offer.
verified 2026-08-14 · source ↗
Vibecode Pestle
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Pestle replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Pestle cost?
Pestle costs about $2.99/month (Pestle Pro, checked 2026-08-14), which is $35.88 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Pestle?
Honestly: native Apple polish, recipe import, hands-free mode, sync, and family sharing; frictionless family sync; retailer and smart-home integrations; large recipe or product database; mobile polish. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Pestle?
Yes: Mealie (Active open-source recipe manager and meal planner for households.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.