Vibecode Cozi Gold
track this build5 phases, 15 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Cozi Gold, share a household calendar, lists, chores, and simple meal notes. The hard boundary is family adoption, mobile apps, reminders, sync, and years of household workflow polish, plus sync, integrations, and household adoption.
You are building a lean indie version of Cozi Gold. 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 ===== # Cozi Gold · indie build A family organiser where the technical work is unglamorous and the failure modes are social: a shared calendar that handles recurrence and time zones correctly, lists that two people can edit at once without losing an item, and a meal plan that produces the shopping list. Estimated effort: **multi-day**. 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 | TypeScript with Fastify and PostgreSQL | a shared household needs a server, since the whole point is more than one person | | Calendar | Events stored as UTC instants or floating local dates, explicitly | an all-day birthday and a six o'clock dinner are different kinds of time | | Lists | Item-level operations, never whole-list writes | two people editing a shopping list in a supermarket is the normal case | | Auth | Household accounts with per-person logins and a simple role split | children need access without administrative capability | | Notifications | Reminders through a queue with per-person quiet hours | notifying a household at seven in the morning is how a tool gets uninstalled | ## 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 server. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Shared household data with concurrent editors needs real transactions. - Get it: Docker or your package manager. - Verify: psql connects and creates a table - [ ] **A recurrence library and a time zone database** · free - Why: Writing recurrence expansion yourself means reimplementing a specification whose edge cases are exactly the bugs you are trying to avoid. - Get it: A maintained library for recurrence rules, plus your platform's zone data kept current. - Verify: A weekly rule expands correctly across a daylight transition in a test - [ ] **A notification channel** (optional) · free tiers cover a household - Why: Phase 5 needs delivery somewhere real, and reminder timing bugs only appear against an actual channel. - Get it: Email through any transactional provider is sufficient, or push if you are building a mobile client later. - Verify: A test reminder arrives at the right time - [ ] **A real household with at least three people** · free, and it will produce feedback - Why: Concurrent list editing, notification fatigue and role restrictions are all social failures that a single tester cannot observe. - Get it: Your own household, using it for real for two weeks including a supermarket trip with two people on the same list. - Verify: Three people have used it for two weeks and complained about something ## 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: - Mobile applications. - Two-way sync with external calendars. - Recipe import from websites. - The advertising-supported free tier. - family adoption, mobile apps, reminders, sync, and years of household workflow polish - 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 Cozi Gold, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Cozi Gold 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 shared household calendar, lists and meal plan that everyone in the house will actually use to replace Cozi Gold. 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. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · a shared household needs a server, since the whole point is more than one person - Calendar: Events stored as UTC instants or floating local dates, explicitly · an all-day birthday and a six o'clock dinner are different kinds of time - Lists: Item-level operations, never whole-list writes · two people editing a shopping list in a supermarket is the normal case - Auth: Household accounts with per-person logins and a simple role split · children need access without administrative capability - Notifications: Reminders through a queue with per-person quiet hours · notifying a household at seven in the morning is how a tool gets uninstalled ### Data model (create this before Phase 1) - households(id, name, timezone, created_at) - people(id, household_id, name, colour, role, login_email) · role is adult or child - events(id, household_id, title, kind, starts_at_utc, ends_at_utc, local_date, all_day, rrule, rrule_zone, created_by) - event_attendees(event_id, person_id) - event_exceptions(event_id, occurrence_date, cancelled, override_json) · because one instance always changes - lists(id, household_id, kind, title) · kind is shopping, todo or packing - list_items(id, list_id, text, checked, checked_by, ordinal, updated_at, deleted_at) - meals(household_id, date, slot, recipe_id, note) · slot is breakfast, lunch or dinner - recipes(id, household_id, title, ingredients_json, servings) - An all-day event stores a local date and no instant, because a birthday does not move when someone travels. ### Phase 1 · A household and the people in it Build: Household accounts with per-person logins, a colour per person, and an adult or child role. Children can see and check things but cannot remove people or delete history. Invite by link, expiring. Trap: A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. Done when: Several people log in separately to one household and their actions are attributed. A child role cannot remove people or delete history, tested by attempting it directly. An invite link expires and cannot be reused. Removing a person preserves what they created. Do not build yet: No calendar, lists or meals. ### Phase 2 · Time that behaves correctly Build: Two kinds of event: an instant with a zone, and an all-day floating local date. Recurrence stored as a rule with its zone and expanded on read, with per-occurrence exceptions. Test both daylight transitions and a travelling household member. Trap: Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. Done when: An all-day event shows on the same date for a household member in another zone, tested with two zones. A weekly recurring event stays at the same local time across both daylight transitions. Editing one occurrence creates an exception without affecting the series. Deleting a series with exceptions removes both. Do not build yet: No lists, meals or reminders. ### Phase 3 · Lists two people can edit at once Build: Item-level add, check and reorder operations, never a whole-list write. Offline edits queue and reconcile on reconnect. Checking is idempotent and records who checked. Trap: Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. Done when: Two clients adding items simultaneously both retain their items, tested with concurrent requests. Checking an item twice is idempotent. Offline edits reconcile on reconnect without duplication, tested by disabling networking on one client. Deleting an item is soft, so a reconnecting client does not resurrect it. Do not build yet: No meals or reminders yet. ### Phase 4 · Meals that produce the shopping list Build: A weekly meal plan with recipes. Generate a shopping list from the plan, combining quantities of the same ingredient and skipping what is already on the list. Adding a generated item must not overwrite manual items. Trap: Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. Done when: A week's plan generates a shopping list combining duplicate ingredients. Regenerating after a plan change preserves manually added items, tested by adding one and regenerating. An ingredient already on the list is not duplicated. Removing a meal removes only its generated items. Do not build yet: No reminders or export yet. ### Phase 5 · Reminders that do not annoy the household Build: Reminders per event and per person, delivered through a queue with per-person quiet hours and a per-person channel. Recurring event reminders must fire per occurrence rather than once for the series. Trap: Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. Done when: A reminder fires for an occurrence rather than once for the series. Quiet hours defer rather than drop a reminder. Only attendees are notified by default, verified with a three-person household. A person can turn off a category of reminder without losing the events. Do not build yet: Export and calendar feeds come last. ### Phase 6 · Calendar feeds and getting your family's data out Build: A read-only calendar feed per person with an unguessable URL that can be revoked. Export everything as JSON and the calendar as a standard file. Deletion removing a household completely. Trap: A calendar feed URL is a bearer credential exposing your family's whereabouts to anyone who has it, and those URLs end up in the sync settings of every device a person has ever used. Make each revocable independently and show when each was last fetched, so an unfamiliar one is visible. Done when: A feed subscribes in an external calendar and shows the correct events including recurrence. Revoking a feed URL stops it working immediately. The export contains events, lists, meals and recipes. Deleting a household removes everything including feeds and queued reminders. Do not build yet: Nothing after this. ### Out of scope (and why) - Mobile applications, which is how a family organiser is overwhelmingly actually used and is most of the hosted product's engineering. - Two-way sync with external calendars, which is far harder than a read-only feed and is where duplication bugs live. - Recipe import from websites, which is a scraping problem against sites that change constantly. - The advertising-supported free tier, which is the business model rather than a feature. ### README must contain - That all-day events store a local date so a birthday does not move across zones - That recurrence is stored as a rule with a zone and expanded on read - That lists use item-level operations so two people can edit at once - That regenerating a shopping list preserves manually added items - That calendar feed URLs are bearer credentials and are individually revocable ===== AGENTS.md ===== # Agent instructions · Cozi Gold indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Events stored as UTC instants or floating local dates, explicitly, Item-level operations, never whole-list writes, Household accounts with per-person logins and a simple role split, Reminders through a queue with per-person quiet hours. 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 - A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. - Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. - Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. - Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. - Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. ===== BUILD_PLAN.md ===== # Build plan · Cozi Gold A family organiser where the technical work is unglamorous and the failure modes are social: a shared calendar that handles recurrence and time zones correctly, lists that two people can edit at once without losing an item, and a meal plan that produces the shopping list. 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 household and the people in it Several people share one household with appropriate access. ### Steps 1. Per-person logins from the start Attribution retrofitted later is a migration nobody enjoys. Files: `src/auth.ts` 2. Two roles, enforced server-side Adult and child. More roles is a family tool nobody configures. Files: `src/roles.ts`, `src/roles.test.ts` 3. Expiring invite links A permanent invite link is a permanent way into your family's calendar. Files: `src/invite.ts` ### Done when - [ ] Several people log in and actions are attributed - [ ] A child role cannot remove people or delete history - [ ] An invite link expires and cannot be reused - [ ] Removing a person preserves their created items ### Watch out - A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. ## Phase 2 · Time that behaves correctly Birthdays do not move and recurring events survive daylight saving. ### Steps 1. Two event kinds, explicitly Instant or floating date. Conflating them is the recurring bug in every calendar. Files: `src/events.ts` 2. Store recurrence rules with a zone, expand on read Materialising occurrences means editing the rule requires rewriting them all. Files: `src/recurrence.ts`, `src/recurrence.test.ts` 3. Exceptions per occurrence One instance always changes. Without exceptions the answer is deleting the series. Files: `src/exceptions.ts` ### Done when - [ ] An all-day event shows on the same date across zones - [ ] Recurrence holds local time across both daylight transitions - [ ] Editing one occurrence creates an exception - [ ] Deleting a series removes its exceptions ### Watch out - Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. ## Phase 3 · Lists two people can edit at once Two people in a supermarket do not lose each other's items. ### Steps 1. Item operations only Add, check, reorder, delete. Never a whole-list write. Files: `src/lists.ts` 2. Queue offline edits with client ids So reconciliation deduplicates rather than duplicating. Files: `src/offline.ts` 3. Soft delete items A reconnecting client with a stale copy otherwise resurrects deleted items. Files: `src/lists.ts` ### Done when - [ ] Concurrent additions from two clients both survive - [ ] Checking twice is idempotent - [ ] Offline edits reconcile without duplication - [ ] Deleted items are not resurrected on reconnect ### Watch out - Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. ## Phase 4 · Meals that produce the shopping list Planning the week's dinners fills the shopping list without retyping. ### Steps 1. Mark generated items with their source So regeneration touches only what it created. Files: `src/generate.ts` 2. Combine same ingredients Quantities added, units normalised where possible, left separate where not. Files: `src/ingredients.ts` 3. Removing a meal removes its items Only its own, and only if nobody checked them off already. Files: `src/generate.ts` ### Done when - [ ] A plan generates a combined shopping list - [ ] Regenerating preserves manual items - [ ] An existing ingredient is not duplicated - [ ] Removing a meal removes only its items ### Watch out - Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. ## Phase 5 · Reminders that do not annoy the household People are reminded usefully and can control when. ### Steps 1. Schedule per occurrence From the expanded recurrence, not from the series row. Files: `src/reminders.ts` 2. Quiet hours defer, not drop A dropped reminder is a missed appointment. Deferral is the correct behaviour. Files: `src/quiet.ts` 3. Notify attendees by default Notifying everyone is how a household mutes the application. Files: `src/notify.ts` ### Done when - [ ] A reminder fires per occurrence - [ ] Quiet hours defer rather than drop - [ ] Only attendees are notified by default - [ ] A person can disable a category without losing events ### Watch out - Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. ## Not in this build - Mobile applications. - Two-way sync with external calendars. - Recipe import from websites. - The advertising-supported free tier. ## After v1, if you want it - A shared household inbox where anyone can drop a note that becomes an event or item - Chores with rotation, which families ask for immediately and which the roles already support - A week-ahead digest, which is where a shared calendar stops needing to be checked ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. A family's calendar and whereabouts. Treat it accordingly. DATABASE_URL=postgres://localhost:5432/household # Required · secret. Signs login sessions. Rotating it logs the household out, which is the intended emergency action. SESSION_SECRET=a random 32-byte hex string # Required. The household's zone. Individuals can differ, which is what phase 2 handles. DEFAULT_TIMEZONE=Europe/London # Required. When reminders defer. The default that stops the tool being muted in week one. QUIET_HOURS_DEFAULT=21:00-07:00 # Required. Where calendar feeds are served. The token in the path is the credential. FEED_BASE_URL=https://home.example.com/feed # Optional · secret. Phase 5 only. Absent means reminders are visible in the application but not delivered. SMTP_URL=smtp://user:pass@host:587
You are building a lean indie version of Cozi Gold. 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 ===== # Cozi Gold · indie build A family organiser where the technical work is unglamorous and the failure modes are social: a shared calendar that handles recurrence and time zones correctly, lists that two people can edit at once without losing an item, and a meal plan that produces the shopping list. Estimated effort: **multi-day**. 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 | TypeScript with Fastify and PostgreSQL | a shared household needs a server, since the whole point is more than one person | | Calendar | Events stored as UTC instants or floating local dates, explicitly | an all-day birthday and a six o'clock dinner are different kinds of time | | Lists | Item-level operations, never whole-list writes | two people editing a shopping list in a supermarket is the normal case | | Auth | Household accounts with per-person logins and a simple role split | children need access without administrative capability | | Notifications | Reminders through a queue with per-person quiet hours | notifying a household at seven in the morning is how a tool gets uninstalled | ## 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 server. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Shared household data with concurrent editors needs real transactions. - Get it: Docker or your package manager. - Verify: psql connects and creates a table - [ ] **A recurrence library and a time zone database** · free - Why: Writing recurrence expansion yourself means reimplementing a specification whose edge cases are exactly the bugs you are trying to avoid. - Get it: A maintained library for recurrence rules, plus your platform's zone data kept current. - Verify: A weekly rule expands correctly across a daylight transition in a test - [ ] **A notification channel** (optional) · free tiers cover a household - Why: Phase 5 needs delivery somewhere real, and reminder timing bugs only appear against an actual channel. - Get it: Email through any transactional provider is sufficient, or push if you are building a mobile client later. - Verify: A test reminder arrives at the right time - [ ] **A real household with at least three people** · free, and it will produce feedback - Why: Concurrent list editing, notification fatigue and role restrictions are all social failures that a single tester cannot observe. - Get it: Your own household, using it for real for two weeks including a supermarket trip with two people on the same list. - Verify: Three people have used it for two weeks and complained about something ## 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: - Mobile applications. - Two-way sync with external calendars. - Recipe import from websites. - The advertising-supported free tier. - family adoption, mobile apps, reminders, sync, and years of household workflow polish - 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 Cozi Gold, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Cozi Gold 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 shared household calendar, lists and meal plan that everyone in the house will actually use to replace Cozi Gold. 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. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · a shared household needs a server, since the whole point is more than one person - Calendar: Events stored as UTC instants or floating local dates, explicitly · an all-day birthday and a six o'clock dinner are different kinds of time - Lists: Item-level operations, never whole-list writes · two people editing a shopping list in a supermarket is the normal case - Auth: Household accounts with per-person logins and a simple role split · children need access without administrative capability - Notifications: Reminders through a queue with per-person quiet hours · notifying a household at seven in the morning is how a tool gets uninstalled ### Data model (create this before Phase 1) - households(id, name, timezone, created_at) - people(id, household_id, name, colour, role, login_email) · role is adult or child - events(id, household_id, title, kind, starts_at_utc, ends_at_utc, local_date, all_day, rrule, rrule_zone, created_by) - event_attendees(event_id, person_id) - event_exceptions(event_id, occurrence_date, cancelled, override_json) · because one instance always changes - lists(id, household_id, kind, title) · kind is shopping, todo or packing - list_items(id, list_id, text, checked, checked_by, ordinal, updated_at, deleted_at) - meals(household_id, date, slot, recipe_id, note) · slot is breakfast, lunch or dinner - recipes(id, household_id, title, ingredients_json, servings) - An all-day event stores a local date and no instant, because a birthday does not move when someone travels. ### Phase 1 · A household and the people in it Build: Household accounts with per-person logins, a colour per person, and an adult or child role. Children can see and check things but cannot remove people or delete history. Invite by link, expiring. Trap: A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. Done when: Several people log in separately to one household and their actions are attributed. A child role cannot remove people or delete history, tested by attempting it directly. An invite link expires and cannot be reused. Removing a person preserves what they created. Do not build yet: No calendar, lists or meals. ### Phase 2 · Time that behaves correctly Build: Two kinds of event: an instant with a zone, and an all-day floating local date. Recurrence stored as a rule with its zone and expanded on read, with per-occurrence exceptions. Test both daylight transitions and a travelling household member. Trap: Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. Done when: An all-day event shows on the same date for a household member in another zone, tested with two zones. A weekly recurring event stays at the same local time across both daylight transitions. Editing one occurrence creates an exception without affecting the series. Deleting a series with exceptions removes both. Do not build yet: No lists, meals or reminders. ### Phase 3 · Lists two people can edit at once Build: Item-level add, check and reorder operations, never a whole-list write. Offline edits queue and reconcile on reconnect. Checking is idempotent and records who checked. Trap: Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. Done when: Two clients adding items simultaneously both retain their items, tested with concurrent requests. Checking an item twice is idempotent. Offline edits reconcile on reconnect without duplication, tested by disabling networking on one client. Deleting an item is soft, so a reconnecting client does not resurrect it. Do not build yet: No meals or reminders yet. ### Phase 4 · Meals that produce the shopping list Build: A weekly meal plan with recipes. Generate a shopping list from the plan, combining quantities of the same ingredient and skipping what is already on the list. Adding a generated item must not overwrite manual items. Trap: Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. Done when: A week's plan generates a shopping list combining duplicate ingredients. Regenerating after a plan change preserves manually added items, tested by adding one and regenerating. An ingredient already on the list is not duplicated. Removing a meal removes only its generated items. Do not build yet: No reminders or export yet. ### Phase 5 · Reminders that do not annoy the household Build: Reminders per event and per person, delivered through a queue with per-person quiet hours and a per-person channel. Recurring event reminders must fire per occurrence rather than once for the series. Trap: Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. Done when: A reminder fires for an occurrence rather than once for the series. Quiet hours defer rather than drop a reminder. Only attendees are notified by default, verified with a three-person household. A person can turn off a category of reminder without losing the events. Do not build yet: Export and calendar feeds come last. ### Phase 6 · Calendar feeds and getting your family's data out Build: A read-only calendar feed per person with an unguessable URL that can be revoked. Export everything as JSON and the calendar as a standard file. Deletion removing a household completely. Trap: A calendar feed URL is a bearer credential exposing your family's whereabouts to anyone who has it, and those URLs end up in the sync settings of every device a person has ever used. Make each revocable independently and show when each was last fetched, so an unfamiliar one is visible. Done when: A feed subscribes in an external calendar and shows the correct events including recurrence. Revoking a feed URL stops it working immediately. The export contains events, lists, meals and recipes. Deleting a household removes everything including feeds and queued reminders. Do not build yet: Nothing after this. ### Out of scope (and why) - Mobile applications, which is how a family organiser is overwhelmingly actually used and is most of the hosted product's engineering. - Two-way sync with external calendars, which is far harder than a read-only feed and is where duplication bugs live. - Recipe import from websites, which is a scraping problem against sites that change constantly. - The advertising-supported free tier, which is the business model rather than a feature. ### README must contain - That all-day events store a local date so a birthday does not move across zones - That recurrence is stored as a rule with a zone and expanded on read - That lists use item-level operations so two people can edit at once - That regenerating a shopping list preserves manually added items - That calendar feed URLs are bearer credentials and are individually revocable ===== AGENTS.md ===== # Agent instructions · Cozi Gold indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Events stored as UTC instants or floating local dates, explicitly, Item-level operations, never whole-list writes, Household accounts with per-person logins and a simple role split, Reminders through a queue with per-person quiet hours. 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 - A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. - Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. - Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. - Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. - Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. ===== BUILD_PLAN.md ===== # Build plan · Cozi Gold A family organiser where the technical work is unglamorous and the failure modes are social: a shared calendar that handles recurrence and time zones correctly, lists that two people can edit at once without losing an item, and a meal plan that produces the shopping list. 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 household and the people in it Several people share one household with appropriate access. ### Steps 1. Per-person logins from the start Attribution retrofitted later is a migration nobody enjoys. Files: `src/auth.ts` 2. Two roles, enforced server-side Adult and child. More roles is a family tool nobody configures. Files: `src/roles.ts`, `src/roles.test.ts` 3. Expiring invite links A permanent invite link is a permanent way into your family's calendar. Files: `src/invite.ts` ### Done when - [ ] Several people log in and actions are attributed - [ ] A child role cannot remove people or delete history - [ ] An invite link expires and cannot be reused - [ ] Removing a person preserves their created items ### Watch out - A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. ## Phase 2 · Time that behaves correctly Birthdays do not move and recurring events survive daylight saving. ### Steps 1. Two event kinds, explicitly Instant or floating date. Conflating them is the recurring bug in every calendar. Files: `src/events.ts` 2. Store recurrence rules with a zone, expand on read Materialising occurrences means editing the rule requires rewriting them all. Files: `src/recurrence.ts`, `src/recurrence.test.ts` 3. Exceptions per occurrence One instance always changes. Without exceptions the answer is deleting the series. Files: `src/exceptions.ts` ### Done when - [ ] An all-day event shows on the same date across zones - [ ] Recurrence holds local time across both daylight transitions - [ ] Editing one occurrence creates an exception - [ ] Deleting a series removes its exceptions ### Watch out - Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. ## Phase 3 · Lists two people can edit at once Two people in a supermarket do not lose each other's items. ### Steps 1. Item operations only Add, check, reorder, delete. Never a whole-list write. Files: `src/lists.ts` 2. Queue offline edits with client ids So reconciliation deduplicates rather than duplicating. Files: `src/offline.ts` 3. Soft delete items A reconnecting client with a stale copy otherwise resurrects deleted items. Files: `src/lists.ts` ### Done when - [ ] Concurrent additions from two clients both survive - [ ] Checking twice is idempotent - [ ] Offline edits reconcile without duplication - [ ] Deleted items are not resurrected on reconnect ### Watch out - Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. ## Phase 4 · Meals that produce the shopping list Planning the week's dinners fills the shopping list without retyping. ### Steps 1. Mark generated items with their source So regeneration touches only what it created. Files: `src/generate.ts` 2. Combine same ingredients Quantities added, units normalised where possible, left separate where not. Files: `src/ingredients.ts` 3. Removing a meal removes its items Only its own, and only if nobody checked them off already. Files: `src/generate.ts` ### Done when - [ ] A plan generates a combined shopping list - [ ] Regenerating preserves manual items - [ ] An existing ingredient is not duplicated - [ ] Removing a meal removes only its items ### Watch out - Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. ## Phase 5 · Reminders that do not annoy the household People are reminded usefully and can control when. ### Steps 1. Schedule per occurrence From the expanded recurrence, not from the series row. Files: `src/reminders.ts` 2. Quiet hours defer, not drop A dropped reminder is a missed appointment. Deferral is the correct behaviour. Files: `src/quiet.ts` 3. Notify attendees by default Notifying everyone is how a household mutes the application. Files: `src/notify.ts` ### Done when - [ ] A reminder fires per occurrence - [ ] Quiet hours defer rather than drop - [ ] Only attendees are notified by default - [ ] A person can disable a category without losing events ### Watch out - Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. ## Not in this build - Mobile applications. - Two-way sync with external calendars. - Recipe import from websites. - The advertising-supported free tier. ## After v1, if you want it - A shared household inbox where anyone can drop a note that becomes an event or item - Chores with rotation, which families ask for immediately and which the roles already support - A week-ahead digest, which is where a shared calendar stops needing to be checked ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. A family's calendar and whereabouts. Treat it accordingly. DATABASE_URL=postgres://localhost:5432/household # Required · secret. Signs login sessions. Rotating it logs the household out, which is the intended emergency action. SESSION_SECRET=a random 32-byte hex string # Required. The household's zone. Individuals can differ, which is what phase 2 handles. DEFAULT_TIMEZONE=Europe/London # Required. When reminders defer. The default that stops the tool being muted in week one. QUIET_HOURS_DEFAULT=21:00-07:00 # Required. Where calendar feeds are served. The token in the path is the credential. FEED_BASE_URL=https://home.example.com/feed # Optional · secret. Phase 5 only. Absent means reminders are visible in the application but not delivered. SMTP_URL=smtp://user:pass@host:587
You are building a production product version of Cozi Gold. 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 ===== # Cozi Gold · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Cozi Gold, share a household calendar, lists, chores, and simple meal notes. The hard boundary is family adoption, mobile apps, reminders, sync, and years of household workflow polish, plus sync, integrations, and household adoption. ## Product outcome A household organiser where birthdays land on the right day for everyone, two people in a supermarket cannot lose each other's items, and the reminders are still switched on after a month. ## 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 - Mobile applications. - Two-way sync with external calendars. - Recipe import from websites. - The advertising-supported free tier. - family adoption, mobile apps, reminders, sync, and years of household workflow polish - frictionless family sync - retailer and smart-home integrations - large recipe or product database - mobile polish ## Success criteria - An all-day event is confirmed to show identically across two zones - Recurrence is confirmed correct across both daylight transitions - Concurrent list additions are confirmed both to survive - Regenerating a shopping list is confirmed to preserve manual items - Revoking a calendar feed is confirmed to stop it immediately ===== BRIEF.md ===== # Build brief · Cozi Gold 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 shared household calendar, lists and meal plan that everyone in the house will actually use to replace Cozi Gold. 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. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · a shared household needs a server, since the whole point is more than one person - Calendar: Events stored as UTC instants or floating local dates, explicitly · an all-day birthday and a six o'clock dinner are different kinds of time - Lists: Item-level operations, never whole-list writes · two people editing a shopping list in a supermarket is the normal case - Auth: Household accounts with per-person logins and a simple role split · children need access without administrative capability - Notifications: Reminders through a queue with per-person quiet hours · notifying a household at seven in the morning is how a tool gets uninstalled ### Data model (create this before Phase 1) - households(id, name, timezone, created_at) - people(id, household_id, name, colour, role, login_email) · role is adult or child - events(id, household_id, title, kind, starts_at_utc, ends_at_utc, local_date, all_day, rrule, rrule_zone, created_by) - event_attendees(event_id, person_id) - event_exceptions(event_id, occurrence_date, cancelled, override_json) · because one instance always changes - lists(id, household_id, kind, title) · kind is shopping, todo or packing - list_items(id, list_id, text, checked, checked_by, ordinal, updated_at, deleted_at) - meals(household_id, date, slot, recipe_id, note) · slot is breakfast, lunch or dinner - recipes(id, household_id, title, ingredients_json, servings) - An all-day event stores a local date and no instant, because a birthday does not move when someone travels. ### Phase 1 · A household and the people in it Build: Household accounts with per-person logins, a colour per person, and an adult or child role. Children can see and check things but cannot remove people or delete history. Invite by link, expiring. Trap: A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. Done when: Several people log in separately to one household and their actions are attributed. A child role cannot remove people or delete history, tested by attempting it directly. An invite link expires and cannot be reused. Removing a person preserves what they created. Do not build yet: No calendar, lists or meals. ### Phase 2 · Time that behaves correctly Build: Two kinds of event: an instant with a zone, and an all-day floating local date. Recurrence stored as a rule with its zone and expanded on read, with per-occurrence exceptions. Test both daylight transitions and a travelling household member. Trap: Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. Done when: An all-day event shows on the same date for a household member in another zone, tested with two zones. A weekly recurring event stays at the same local time across both daylight transitions. Editing one occurrence creates an exception without affecting the series. Deleting a series with exceptions removes both. Do not build yet: No lists, meals or reminders. ### Phase 3 · Lists two people can edit at once Build: Item-level add, check and reorder operations, never a whole-list write. Offline edits queue and reconcile on reconnect. Checking is idempotent and records who checked. Trap: Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. Done when: Two clients adding items simultaneously both retain their items, tested with concurrent requests. Checking an item twice is idempotent. Offline edits reconcile on reconnect without duplication, tested by disabling networking on one client. Deleting an item is soft, so a reconnecting client does not resurrect it. Do not build yet: No meals or reminders yet. ### Phase 4 · Meals that produce the shopping list Build: A weekly meal plan with recipes. Generate a shopping list from the plan, combining quantities of the same ingredient and skipping what is already on the list. Adding a generated item must not overwrite manual items. Trap: Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. Done when: A week's plan generates a shopping list combining duplicate ingredients. Regenerating after a plan change preserves manually added items, tested by adding one and regenerating. An ingredient already on the list is not duplicated. Removing a meal removes only its generated items. Do not build yet: No reminders or export yet. ### Phase 5 · Reminders that do not annoy the household Build: Reminders per event and per person, delivered through a queue with per-person quiet hours and a per-person channel. Recurring event reminders must fire per occurrence rather than once for the series. Trap: Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. Done when: A reminder fires for an occurrence rather than once for the series. Quiet hours defer rather than drop a reminder. Only attendees are notified by default, verified with a three-person household. A person can turn off a category of reminder without losing the events. Do not build yet: Export and calendar feeds come last. ### Phase 6 · Calendar feeds and getting your family's data out Build: A read-only calendar feed per person with an unguessable URL that can be revoked. Export everything as JSON and the calendar as a standard file. Deletion removing a household completely. Trap: A calendar feed URL is a bearer credential exposing your family's whereabouts to anyone who has it, and those URLs end up in the sync settings of every device a person has ever used. Make each revocable independently and show when each was last fetched, so an unfamiliar one is visible. Done when: A feed subscribes in an external calendar and shows the correct events including recurrence. Revoking a feed URL stops it working immediately. The export contains events, lists, meals and recipes. Deleting a household removes everything including feeds and queued reminders. Do not build yet: Nothing after this. ### Out of scope (and why) - Mobile applications, which is how a family organiser is overwhelmingly actually used and is most of the hosted product's engineering. - Two-way sync with external calendars, which is far harder than a read-only feed and is where duplication bugs live. - Recipe import from websites, which is a scraping problem against sites that change constantly. - The advertising-supported free tier, which is the business model rather than a feature. ### README must contain - That all-day events store a local date so a birthday does not move across zones - That recurrence is stored as a rule with a zone and expanded on read - That lists use item-level operations so two people can edit at once - That regenerating a shopping list preserves manually added items - That calendar feed URLs are bearer credentials and are individually revocable ===== ARCHITECTURE.md ===== # Architecture · Cozi Gold ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | a shared household needs a server, since the whole point is more than one person | | Calendar | Events stored as UTC instants or floating local dates, explicitly | an all-day birthday and a six o'clock dinner are different kinds of time | | Lists | Item-level operations, never whole-list writes | two people editing a shopping list in a supermarket is the normal case | | Auth | Household accounts with per-person logins and a simple role split | children need access without administrative capability | | Notifications | Reminders through a queue with per-person quiet hours | notifying a household at seven in the morning is how a tool gets uninstalled | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Household | People, roles and invites. | Not swappable for a shared login, which loses attribution and child access. | | Calendar | Two event kinds, recurrence and exceptions. | Any recurrence library. Storing all-day as a floating date is the requirement. | | Lists | Item-level concurrent editing. | Any implementation, provided it never writes a whole list. | | Meals | Plans and generated shopping items. | Any generator, provided generated items are marked and manual ones survive. | | Reminders | Per-occurrence delivery with quiet hours. | Any channel. Defaulting to attendees is what keeps notifications on. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · A family's calendar and whereabouts. Treat it accordingly. - `SESSION_SECRET` · required, secret · Signs login sessions. Rotating it logs the household out, which is the intended emergency action. - `DEFAULT_TIMEZONE` · required · The household's zone. Individuals can differ, which is what phase 2 handles. - `QUIET_HOURS_DEFAULT` · required · When reminders defer. The default that stops the tool being muted in week one. - `FEED_BASE_URL` · required · Where calendar feeds are served. The token in the path is the credential. - `SMTP_URL` · optional, secret · Phase 5 only. Absent means reminders are visible in the application but not delivered. ## 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 · Cozi Gold product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Events stored as UTC instants or floating local dates, explicitly, Item-level operations, never whole-list writes, Household accounts with per-person logins and a simple role split, Reminders through a queue with per-person quiet hours. - 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 - A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. - Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. - Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. - Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. - Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. - A calendar feed URL is a bearer credential exposing your family's whereabouts to anyone who has it, and those URLs end up in the sync settings of every device a person has ever used. Make each revocable independently and show when each was last fetched, so an unfamiliar one is visible. ===== MILESTONES.md ===== # Delivery milestones · Cozi Gold Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A household and the people in it Several people share one household with appropriate access. ### Steps 1. Per-person logins from the start Attribution retrofitted later is a migration nobody enjoys. Files: `src/auth.ts` 2. Two roles, enforced server-side Adult and child. More roles is a family tool nobody configures. Files: `src/roles.ts`, `src/roles.test.ts` 3. Expiring invite links A permanent invite link is a permanent way into your family's calendar. Files: `src/invite.ts` ### Done when - [ ] Several people log in and actions are attributed - [ ] A child role cannot remove people or delete history - [ ] An invite link expires and cannot be reused - [ ] Removing a person preserves their created items ### Watch out - A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. ## M2 · Time that behaves correctly Birthdays do not move and recurring events survive daylight saving. ### Steps 1. Two event kinds, explicitly Instant or floating date. Conflating them is the recurring bug in every calendar. Files: `src/events.ts` 2. Store recurrence rules with a zone, expand on read Materialising occurrences means editing the rule requires rewriting them all. Files: `src/recurrence.ts`, `src/recurrence.test.ts` 3. Exceptions per occurrence One instance always changes. Without exceptions the answer is deleting the series. Files: `src/exceptions.ts` ### Done when - [ ] An all-day event shows on the same date across zones - [ ] Recurrence holds local time across both daylight transitions - [ ] Editing one occurrence creates an exception - [ ] Deleting a series removes its exceptions ### Watch out - Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. ## M3 · Lists two people can edit at once Two people in a supermarket do not lose each other's items. ### Steps 1. Item operations only Add, check, reorder, delete. Never a whole-list write. Files: `src/lists.ts` 2. Queue offline edits with client ids So reconciliation deduplicates rather than duplicating. Files: `src/offline.ts` 3. Soft delete items A reconnecting client with a stale copy otherwise resurrects deleted items. Files: `src/lists.ts` ### Done when - [ ] Concurrent additions from two clients both survive - [ ] Checking twice is idempotent - [ ] Offline edits reconcile without duplication - [ ] Deleted items are not resurrected on reconnect ### Watch out - Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. ## M4 · Meals that produce the shopping list Planning the week's dinners fills the shopping list without retyping. ### Steps 1. Mark generated items with their source So regeneration touches only what it created. Files: `src/generate.ts` 2. Combine same ingredients Quantities added, units normalised where possible, left separate where not. Files: `src/ingredients.ts` 3. Removing a meal removes its items Only its own, and only if nobody checked them off already. Files: `src/generate.ts` ### Done when - [ ] A plan generates a combined shopping list - [ ] Regenerating preserves manual items - [ ] An existing ingredient is not duplicated - [ ] Removing a meal removes only its items ### Watch out - Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. ## M5 · Reminders that do not annoy the household People are reminded usefully and can control when. ### Steps 1. Schedule per occurrence From the expanded recurrence, not from the series row. Files: `src/reminders.ts` 2. Quiet hours defer, not drop A dropped reminder is a missed appointment. Deferral is the correct behaviour. Files: `src/quiet.ts` 3. Notify attendees by default Notifying everyone is how a household mutes the application. Files: `src/notify.ts` ### Done when - [ ] A reminder fires per occurrence - [ ] Quiet hours defer rather than drop - [ ] Only attendees are notified by default - [ ] A person can disable a category without losing events ### Watch out - Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. ## M6 · Calendar feeds and getting your family's data out (production only) The household calendar appears in whatever people already use, and the data can leave. ### Steps 1. One revocable feed per person With last-fetched shown, so a forgotten subscription is visible. Files: `src/feed.ts` 2. Expand recurrence correctly in the feed Including exceptions. External calendars are strict about this. Files: `src/feed.ts` 3. Complete export and deletion It is a family's data. Both directions should work fully. Files: `src/export.ts`, `src/deletion.ts` ### Done when - [ ] A feed subscribes and shows recurrence correctly - [ ] Revoking a feed stops it immediately - [ ] Export contains events, lists, meals and recipes - [ ] Deletion removes feeds and queued reminders ### Watch out - A calendar feed URL is a bearer credential exposing your family's whereabouts to anyone who has it, and those URLs end up in the sync settings of every device a person has ever used. Make each revocable independently and show when each was last fetched, so an unfamiliar one is visible. ===== OPERATIONS.md ===== # Operations · Cozi Gold ## Backup A family calendar with years of history is not reconstructible. Nightly backups with a tested restore, and be careful where they live, since this is location data about children. ## Restore Restore and confirm recurrence exceptions survived, since those are the easiest thing to lose and their loss reinstates cancelled events, which is worse than an outage. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Reminder delivery success and lateness, plus list operation conflicts. A rising conflict rate usually means a client is writing whole lists somewhere. ## Incident checklist If an event shows on the wrong day for one person, an all-day event is stored as an instant. If a household reports missing shopping items, a whole-list write has crept in. Both are data model defects rather than display bugs. 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 - [ ] An all-day event is confirmed to show identically across two zones - [ ] Recurrence is confirmed correct across both daylight transitions - [ ] Concurrent list additions are confirmed both to survive - [ ] Regenerating a shopping list is confirmed to preserve manual items - [ ] Revoking a calendar feed is confirmed to stop it immediately ## Launch constraint Do not market omitted Cozi Gold 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 · secret. A family's calendar and whereabouts. Treat it accordingly. DATABASE_URL=postgres://localhost:5432/household # Required · secret. Signs login sessions. Rotating it logs the household out, which is the intended emergency action. SESSION_SECRET=a random 32-byte hex string # Required. The household's zone. Individuals can differ, which is what phase 2 handles. DEFAULT_TIMEZONE=Europe/London # Required. When reminders defer. The default that stops the tool being muted in week one. QUIET_HOURS_DEFAULT=21:00-07:00 # Required. Where calendar feeds are served. The token in the path is the credential. FEED_BASE_URL=https://home.example.com/feed # Optional · secret. Phase 5 only. Absent means reminders are visible in the application but not delivered. SMTP_URL=smtp://user:pass@host:587
# Cozi Gold · indie build A family organiser where the technical work is unglamorous and the failure modes are social: a shared calendar that handles recurrence and time zones correctly, lists that two people can edit at once without losing an item, and a meal plan that produces the shopping list. Estimated effort: **multi-day**. 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 | TypeScript with Fastify and PostgreSQL | a shared household needs a server, since the whole point is more than one person | | Calendar | Events stored as UTC instants or floating local dates, explicitly | an all-day birthday and a six o'clock dinner are different kinds of time | | Lists | Item-level operations, never whole-list writes | two people editing a shopping list in a supermarket is the normal case | | Auth | Household accounts with per-person logins and a simple role split | children need access without administrative capability | | Notifications | Reminders through a queue with per-person quiet hours | notifying a household at seven in the morning is how a tool gets uninstalled | ## 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 server. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Shared household data with concurrent editors needs real transactions. - Get it: Docker or your package manager. - Verify: psql connects and creates a table - [ ] **A recurrence library and a time zone database** · free - Why: Writing recurrence expansion yourself means reimplementing a specification whose edge cases are exactly the bugs you are trying to avoid. - Get it: A maintained library for recurrence rules, plus your platform's zone data kept current. - Verify: A weekly rule expands correctly across a daylight transition in a test - [ ] **A notification channel** (optional) · free tiers cover a household - Why: Phase 5 needs delivery somewhere real, and reminder timing bugs only appear against an actual channel. - Get it: Email through any transactional provider is sufficient, or push if you are building a mobile client later. - Verify: A test reminder arrives at the right time - [ ] **A real household with at least three people** · free, and it will produce feedback - Why: Concurrent list editing, notification fatigue and role restrictions are all social failures that a single tester cannot observe. - Get it: Your own household, using it for real for two weeks including a supermarket trip with two people on the same list. - Verify: Three people have used it for two weeks and complained about something ## 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: - Mobile applications. - Two-way sync with external calendars. - Recipe import from websites. - The advertising-supported free tier. - family adoption, mobile apps, reminders, sync, and years of household workflow polish - 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 Cozi Gold, and the README should say so rather than pretend.
# Build brief · Cozi Gold 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 shared household calendar, lists and meal plan that everyone in the house will actually use to replace Cozi Gold. 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. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · a shared household needs a server, since the whole point is more than one person - Calendar: Events stored as UTC instants or floating local dates, explicitly · an all-day birthday and a six o'clock dinner are different kinds of time - Lists: Item-level operations, never whole-list writes · two people editing a shopping list in a supermarket is the normal case - Auth: Household accounts with per-person logins and a simple role split · children need access without administrative capability - Notifications: Reminders through a queue with per-person quiet hours · notifying a household at seven in the morning is how a tool gets uninstalled ### Data model (create this before Phase 1) - households(id, name, timezone, created_at) - people(id, household_id, name, colour, role, login_email) · role is adult or child - events(id, household_id, title, kind, starts_at_utc, ends_at_utc, local_date, all_day, rrule, rrule_zone, created_by) - event_attendees(event_id, person_id) - event_exceptions(event_id, occurrence_date, cancelled, override_json) · because one instance always changes - lists(id, household_id, kind, title) · kind is shopping, todo or packing - list_items(id, list_id, text, checked, checked_by, ordinal, updated_at, deleted_at) - meals(household_id, date, slot, recipe_id, note) · slot is breakfast, lunch or dinner - recipes(id, household_id, title, ingredients_json, servings) - An all-day event stores a local date and no instant, because a birthday does not move when someone travels. ### Phase 1 · A household and the people in it Build: Household accounts with per-person logins, a colour per person, and an adult or child role. Children can see and check things but cannot remove people or delete history. Invite by link, expiring. Trap: A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. Done when: Several people log in separately to one household and their actions are attributed. A child role cannot remove people or delete history, tested by attempting it directly. An invite link expires and cannot be reused. Removing a person preserves what they created. Do not build yet: No calendar, lists or meals. ### Phase 2 · Time that behaves correctly Build: Two kinds of event: an instant with a zone, and an all-day floating local date. Recurrence stored as a rule with its zone and expanded on read, with per-occurrence exceptions. Test both daylight transitions and a travelling household member. Trap: Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. Done when: An all-day event shows on the same date for a household member in another zone, tested with two zones. A weekly recurring event stays at the same local time across both daylight transitions. Editing one occurrence creates an exception without affecting the series. Deleting a series with exceptions removes both. Do not build yet: No lists, meals or reminders. ### Phase 3 · Lists two people can edit at once Build: Item-level add, check and reorder operations, never a whole-list write. Offline edits queue and reconcile on reconnect. Checking is idempotent and records who checked. Trap: Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. Done when: Two clients adding items simultaneously both retain their items, tested with concurrent requests. Checking an item twice is idempotent. Offline edits reconcile on reconnect without duplication, tested by disabling networking on one client. Deleting an item is soft, so a reconnecting client does not resurrect it. Do not build yet: No meals or reminders yet. ### Phase 4 · Meals that produce the shopping list Build: A weekly meal plan with recipes. Generate a shopping list from the plan, combining quantities of the same ingredient and skipping what is already on the list. Adding a generated item must not overwrite manual items. Trap: Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. Done when: A week's plan generates a shopping list combining duplicate ingredients. Regenerating after a plan change preserves manually added items, tested by adding one and regenerating. An ingredient already on the list is not duplicated. Removing a meal removes only its generated items. Do not build yet: No reminders or export yet. ### Phase 5 · Reminders that do not annoy the household Build: Reminders per event and per person, delivered through a queue with per-person quiet hours and a per-person channel. Recurring event reminders must fire per occurrence rather than once for the series. Trap: Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. Done when: A reminder fires for an occurrence rather than once for the series. Quiet hours defer rather than drop a reminder. Only attendees are notified by default, verified with a three-person household. A person can turn off a category of reminder without losing the events. Do not build yet: Export and calendar feeds come last. ### Phase 6 · Calendar feeds and getting your family's data out Build: A read-only calendar feed per person with an unguessable URL that can be revoked. Export everything as JSON and the calendar as a standard file. Deletion removing a household completely. Trap: A calendar feed URL is a bearer credential exposing your family's whereabouts to anyone who has it, and those URLs end up in the sync settings of every device a person has ever used. Make each revocable independently and show when each was last fetched, so an unfamiliar one is visible. Done when: A feed subscribes in an external calendar and shows the correct events including recurrence. Revoking a feed URL stops it working immediately. The export contains events, lists, meals and recipes. Deleting a household removes everything including feeds and queued reminders. Do not build yet: Nothing after this. ### Out of scope (and why) - Mobile applications, which is how a family organiser is overwhelmingly actually used and is most of the hosted product's engineering. - Two-way sync with external calendars, which is far harder than a read-only feed and is where duplication bugs live. - Recipe import from websites, which is a scraping problem against sites that change constantly. - The advertising-supported free tier, which is the business model rather than a feature. ### README must contain - That all-day events store a local date so a birthday does not move across zones - That recurrence is stored as a rule with a zone and expanded on read - That lists use item-level operations so two people can edit at once - That regenerating a shopping list preserves manually added items - That calendar feed URLs are bearer credentials and are individually revocable
# Agent instructions · Cozi Gold indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Events stored as UTC instants or floating local dates, explicitly, Item-level operations, never whole-list writes, Household accounts with per-person logins and a simple role split, Reminders through a queue with per-person quiet hours. 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 - A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. - Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. - Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. - Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. - Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable.
# Build plan · Cozi Gold A family organiser where the technical work is unglamorous and the failure modes are social: a shared calendar that handles recurrence and time zones correctly, lists that two people can edit at once without losing an item, and a meal plan that produces the shopping list. 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 household and the people in it Several people share one household with appropriate access. ### Steps 1. Per-person logins from the start Attribution retrofitted later is a migration nobody enjoys. Files: `src/auth.ts` 2. Two roles, enforced server-side Adult and child. More roles is a family tool nobody configures. Files: `src/roles.ts`, `src/roles.test.ts` 3. Expiring invite links A permanent invite link is a permanent way into your family's calendar. Files: `src/invite.ts` ### Done when - [ ] Several people log in and actions are attributed - [ ] A child role cannot remove people or delete history - [ ] An invite link expires and cannot be reused - [ ] Removing a person preserves their created items ### Watch out - A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. ## Phase 2 · Time that behaves correctly Birthdays do not move and recurring events survive daylight saving. ### Steps 1. Two event kinds, explicitly Instant or floating date. Conflating them is the recurring bug in every calendar. Files: `src/events.ts` 2. Store recurrence rules with a zone, expand on read Materialising occurrences means editing the rule requires rewriting them all. Files: `src/recurrence.ts`, `src/recurrence.test.ts` 3. Exceptions per occurrence One instance always changes. Without exceptions the answer is deleting the series. Files: `src/exceptions.ts` ### Done when - [ ] An all-day event shows on the same date across zones - [ ] Recurrence holds local time across both daylight transitions - [ ] Editing one occurrence creates an exception - [ ] Deleting a series removes its exceptions ### Watch out - Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. ## Phase 3 · Lists two people can edit at once Two people in a supermarket do not lose each other's items. ### Steps 1. Item operations only Add, check, reorder, delete. Never a whole-list write. Files: `src/lists.ts` 2. Queue offline edits with client ids So reconciliation deduplicates rather than duplicating. Files: `src/offline.ts` 3. Soft delete items A reconnecting client with a stale copy otherwise resurrects deleted items. Files: `src/lists.ts` ### Done when - [ ] Concurrent additions from two clients both survive - [ ] Checking twice is idempotent - [ ] Offline edits reconcile without duplication - [ ] Deleted items are not resurrected on reconnect ### Watch out - Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. ## Phase 4 · Meals that produce the shopping list Planning the week's dinners fills the shopping list without retyping. ### Steps 1. Mark generated items with their source So regeneration touches only what it created. Files: `src/generate.ts` 2. Combine same ingredients Quantities added, units normalised where possible, left separate where not. Files: `src/ingredients.ts` 3. Removing a meal removes its items Only its own, and only if nobody checked them off already. Files: `src/generate.ts` ### Done when - [ ] A plan generates a combined shopping list - [ ] Regenerating preserves manual items - [ ] An existing ingredient is not duplicated - [ ] Removing a meal removes only its items ### Watch out - Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. ## Phase 5 · Reminders that do not annoy the household People are reminded usefully and can control when. ### Steps 1. Schedule per occurrence From the expanded recurrence, not from the series row. Files: `src/reminders.ts` 2. Quiet hours defer, not drop A dropped reminder is a missed appointment. Deferral is the correct behaviour. Files: `src/quiet.ts` 3. Notify attendees by default Notifying everyone is how a household mutes the application. Files: `src/notify.ts` ### Done when - [ ] A reminder fires per occurrence - [ ] Quiet hours defer rather than drop - [ ] Only attendees are notified by default - [ ] A person can disable a category without losing events ### Watch out - Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. ## Not in this build - Mobile applications. - Two-way sync with external calendars. - Recipe import from websites. - The advertising-supported free tier. ## After v1, if you want it - A shared household inbox where anyone can drop a note that becomes an event or item - Chores with rotation, which families ask for immediately and which the roles already support - A week-ahead digest, which is where a shared calendar stops needing to be checked
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. A family's calendar and whereabouts. Treat it accordingly. DATABASE_URL=postgres://localhost:5432/household # Required · secret. Signs login sessions. Rotating it logs the household out, which is the intended emergency action. SESSION_SECRET=a random 32-byte hex string # Required. The household's zone. Individuals can differ, which is what phase 2 handles. DEFAULT_TIMEZONE=Europe/London # Required. When reminders defer. The default that stops the tool being muted in week one. QUIET_HOURS_DEFAULT=21:00-07:00 # Required. Where calendar feeds are served. The token in the path is the credential. FEED_BASE_URL=https://home.example.com/feed # Optional · secret. Phase 5 only. Absent means reminders are visible in the application but not delivered. SMTP_URL=smtp://user:pass@host:587
# Cozi Gold · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Cozi Gold, share a household calendar, lists, chores, and simple meal notes. The hard boundary is family adoption, mobile apps, reminders, sync, and years of household workflow polish, plus sync, integrations, and household adoption. ## Product outcome A household organiser where birthdays land on the right day for everyone, two people in a supermarket cannot lose each other's items, and the reminders are still switched on after a month. ## 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 - Mobile applications. - Two-way sync with external calendars. - Recipe import from websites. - The advertising-supported free tier. - family adoption, mobile apps, reminders, sync, and years of household workflow polish - frictionless family sync - retailer and smart-home integrations - large recipe or product database - mobile polish ## Success criteria - An all-day event is confirmed to show identically across two zones - Recurrence is confirmed correct across both daylight transitions - Concurrent list additions are confirmed both to survive - Regenerating a shopping list is confirmed to preserve manual items - Revoking a calendar feed is confirmed to stop it immediately
# Build brief · Cozi Gold 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 shared household calendar, lists and meal plan that everyone in the house will actually use to replace Cozi Gold. 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. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Fastify and PostgreSQL · a shared household needs a server, since the whole point is more than one person - Calendar: Events stored as UTC instants or floating local dates, explicitly · an all-day birthday and a six o'clock dinner are different kinds of time - Lists: Item-level operations, never whole-list writes · two people editing a shopping list in a supermarket is the normal case - Auth: Household accounts with per-person logins and a simple role split · children need access without administrative capability - Notifications: Reminders through a queue with per-person quiet hours · notifying a household at seven in the morning is how a tool gets uninstalled ### Data model (create this before Phase 1) - households(id, name, timezone, created_at) - people(id, household_id, name, colour, role, login_email) · role is adult or child - events(id, household_id, title, kind, starts_at_utc, ends_at_utc, local_date, all_day, rrule, rrule_zone, created_by) - event_attendees(event_id, person_id) - event_exceptions(event_id, occurrence_date, cancelled, override_json) · because one instance always changes - lists(id, household_id, kind, title) · kind is shopping, todo or packing - list_items(id, list_id, text, checked, checked_by, ordinal, updated_at, deleted_at) - meals(household_id, date, slot, recipe_id, note) · slot is breakfast, lunch or dinner - recipes(id, household_id, title, ingredients_json, servings) - An all-day event stores a local date and no instant, because a birthday does not move when someone travels. ### Phase 1 · A household and the people in it Build: Household accounts with per-person logins, a colour per person, and an adult or child role. Children can see and check things but cannot remove people or delete history. Invite by link, expiring. Trap: A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. Done when: Several people log in separately to one household and their actions are attributed. A child role cannot remove people or delete history, tested by attempting it directly. An invite link expires and cannot be reused. Removing a person preserves what they created. Do not build yet: No calendar, lists or meals. ### Phase 2 · Time that behaves correctly Build: Two kinds of event: an instant with a zone, and an all-day floating local date. Recurrence stored as a rule with its zone and expanded on read, with per-occurrence exceptions. Test both daylight transitions and a travelling household member. Trap: Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. Done when: An all-day event shows on the same date for a household member in another zone, tested with two zones. A weekly recurring event stays at the same local time across both daylight transitions. Editing one occurrence creates an exception without affecting the series. Deleting a series with exceptions removes both. Do not build yet: No lists, meals or reminders. ### Phase 3 · Lists two people can edit at once Build: Item-level add, check and reorder operations, never a whole-list write. Offline edits queue and reconcile on reconnect. Checking is idempotent and records who checked. Trap: Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. Done when: Two clients adding items simultaneously both retain their items, tested with concurrent requests. Checking an item twice is idempotent. Offline edits reconcile on reconnect without duplication, tested by disabling networking on one client. Deleting an item is soft, so a reconnecting client does not resurrect it. Do not build yet: No meals or reminders yet. ### Phase 4 · Meals that produce the shopping list Build: A weekly meal plan with recipes. Generate a shopping list from the plan, combining quantities of the same ingredient and skipping what is already on the list. Adding a generated item must not overwrite manual items. Trap: Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. Done when: A week's plan generates a shopping list combining duplicate ingredients. Regenerating after a plan change preserves manually added items, tested by adding one and regenerating. An ingredient already on the list is not duplicated. Removing a meal removes only its generated items. Do not build yet: No reminders or export yet. ### Phase 5 · Reminders that do not annoy the household Build: Reminders per event and per person, delivered through a queue with per-person quiet hours and a per-person channel. Recurring event reminders must fire per occurrence rather than once for the series. Trap: Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. Done when: A reminder fires for an occurrence rather than once for the series. Quiet hours defer rather than drop a reminder. Only attendees are notified by default, verified with a three-person household. A person can turn off a category of reminder without losing the events. Do not build yet: Export and calendar feeds come last. ### Phase 6 · Calendar feeds and getting your family's data out Build: A read-only calendar feed per person with an unguessable URL that can be revoked. Export everything as JSON and the calendar as a standard file. Deletion removing a household completely. Trap: A calendar feed URL is a bearer credential exposing your family's whereabouts to anyone who has it, and those URLs end up in the sync settings of every device a person has ever used. Make each revocable independently and show when each was last fetched, so an unfamiliar one is visible. Done when: A feed subscribes in an external calendar and shows the correct events including recurrence. Revoking a feed URL stops it working immediately. The export contains events, lists, meals and recipes. Deleting a household removes everything including feeds and queued reminders. Do not build yet: Nothing after this. ### Out of scope (and why) - Mobile applications, which is how a family organiser is overwhelmingly actually used and is most of the hosted product's engineering. - Two-way sync with external calendars, which is far harder than a read-only feed and is where duplication bugs live. - Recipe import from websites, which is a scraping problem against sites that change constantly. - The advertising-supported free tier, which is the business model rather than a feature. ### README must contain - That all-day events store a local date so a birthday does not move across zones - That recurrence is stored as a rule with a zone and expanded on read - That lists use item-level operations so two people can edit at once - That regenerating a shopping list preserves manually added items - That calendar feed URLs are bearer credentials and are individually revocable
# Architecture · Cozi Gold ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Fastify and PostgreSQL | a shared household needs a server, since the whole point is more than one person | | Calendar | Events stored as UTC instants or floating local dates, explicitly | an all-day birthday and a six o'clock dinner are different kinds of time | | Lists | Item-level operations, never whole-list writes | two people editing a shopping list in a supermarket is the normal case | | Auth | Household accounts with per-person logins and a simple role split | children need access without administrative capability | | Notifications | Reminders through a queue with per-person quiet hours | notifying a household at seven in the morning is how a tool gets uninstalled | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Household | People, roles and invites. | Not swappable for a shared login, which loses attribution and child access. | | Calendar | Two event kinds, recurrence and exceptions. | Any recurrence library. Storing all-day as a floating date is the requirement. | | Lists | Item-level concurrent editing. | Any implementation, provided it never writes a whole list. | | Meals | Plans and generated shopping items. | Any generator, provided generated items are marked and manual ones survive. | | Reminders | Per-occurrence delivery with quiet hours. | Any channel. Defaulting to attendees is what keeps notifications on. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · A family's calendar and whereabouts. Treat it accordingly. - `SESSION_SECRET` · required, secret · Signs login sessions. Rotating it logs the household out, which is the intended emergency action. - `DEFAULT_TIMEZONE` · required · The household's zone. Individuals can differ, which is what phase 2 handles. - `QUIET_HOURS_DEFAULT` · required · When reminders defer. The default that stops the tool being muted in week one. - `FEED_BASE_URL` · required · Where calendar feeds are served. The token in the path is the credential. - `SMTP_URL` · optional, secret · Phase 5 only. Absent means reminders are visible in the application but not delivered. ## 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 · Cozi Gold product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Fastify and PostgreSQL, Events stored as UTC instants or floating local dates, explicitly, Item-level operations, never whole-list writes, Household accounts with per-person logins and a simple role split, Reminders through a queue with per-person quiet hours. - 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 - A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. - Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. - Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. - Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. - Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. - A calendar feed URL is a bearer credential exposing your family's whereabouts to anyone who has it, and those URLs end up in the sync settings of every device a person has ever used. Make each revocable independently and show when each was last fetched, so an unfamiliar one is visible.
# Delivery milestones · Cozi Gold Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A household and the people in it Several people share one household with appropriate access. ### Steps 1. Per-person logins from the start Attribution retrofitted later is a migration nobody enjoys. Files: `src/auth.ts` 2. Two roles, enforced server-side Adult and child. More roles is a family tool nobody configures. Files: `src/roles.ts`, `src/roles.test.ts` 3. Expiring invite links A permanent invite link is a permanent way into your family's calendar. Files: `src/invite.ts` ### Done when - [ ] Several people log in and actions are attributed - [ ] A child role cannot remove people or delete history - [ ] An invite link expires and cannot be reused - [ ] Removing a person preserves their created items ### Watch out - A single shared login for the household means you can never tell who added an event or checked an item, and there is no way to give a child limited access, which is the whole reason a family tool needs accounts at all. Per-person logins from the start; retrofitting attribution is painful. ## M2 · Time that behaves correctly Birthdays do not move and recurring events survive daylight saving. ### Steps 1. Two event kinds, explicitly Instant or floating date. Conflating them is the recurring bug in every calendar. Files: `src/events.ts` 2. Store recurrence rules with a zone, expand on read Materialising occurrences means editing the rule requires rewriting them all. Files: `src/recurrence.ts`, `src/recurrence.test.ts` 3. Exceptions per occurrence One instance always changes. Without exceptions the answer is deleting the series. Files: `src/exceptions.ts` ### Done when - [ ] An all-day event shows on the same date across zones - [ ] Recurrence holds local time across both daylight transitions - [ ] Editing one occurrence creates an exception - [ ] Deleting a series removes its exceptions ### Watch out - Storing an all-day event as midnight UTC makes a birthday show on the wrong day for anyone west of the meridian, which in a family with a member abroad is an immediate and embarrassing bug. Store all-day events as a local date with no instant at all. ## M3 · Lists two people can edit at once Two people in a supermarket do not lose each other's items. ### Steps 1. Item operations only Add, check, reorder, delete. Never a whole-list write. Files: `src/lists.ts` 2. Queue offline edits with client ids So reconciliation deduplicates rather than duplicating. Files: `src/offline.ts` 3. Soft delete items A reconnecting client with a stale copy otherwise resurrects deleted items. Files: `src/lists.ts` ### Done when - [ ] Concurrent additions from two clients both survive - [ ] Checking twice is idempotent - [ ] Offline edits reconcile without duplication - [ ] Deleted items are not resurrected on reconnect ### Watch out - Saving the whole list on every change means the second person's save silently discards the first person's additions, and in a supermarket with two people on the same list this happens within minutes. Operate on items, never on the list as a document. ## M4 · Meals that produce the shopping list Planning the week's dinners fills the shopping list without retyping. ### Steps 1. Mark generated items with their source So regeneration touches only what it created. Files: `src/generate.ts` 2. Combine same ingredients Quantities added, units normalised where possible, left separate where not. Files: `src/ingredients.ts` 3. Removing a meal removes its items Only its own, and only if nobody checked them off already. Files: `src/generate.ts` ### Done when - [ ] A plan generates a combined shopping list - [ ] Regenerating preserves manual items - [ ] An existing ingredient is not duplicated - [ ] Removing a meal removes only its items ### Watch out - Regenerating the shopping list after a plan change wipes the items someone added by hand, which is the household's actual shopping list and the thing they will be most annoyed to lose. Mark generated items with their source and only ever replace those. ## M5 · Reminders that do not annoy the household People are reminded usefully and can control when. ### Steps 1. Schedule per occurrence From the expanded recurrence, not from the series row. Files: `src/reminders.ts` 2. Quiet hours defer, not drop A dropped reminder is a missed appointment. Deferral is the correct behaviour. Files: `src/quiet.ts` 3. Notify attendees by default Notifying everyone is how a household mutes the application. Files: `src/notify.ts` ### Done when - [ ] A reminder fires per occurrence - [ ] Quiet hours defer rather than drop - [ ] Only attendees are notified by default - [ ] A person can disable a category without losing events ### Watch out - Sending a reminder for every event to everyone in the household means the notifications are muted within a week and the tool has quietly stopped working. Default to notifying only attendees, respect quiet hours per person, and make a child's notifications adult-configurable. ## M6 · Calendar feeds and getting your family's data out (production only) The household calendar appears in whatever people already use, and the data can leave. ### Steps 1. One revocable feed per person With last-fetched shown, so a forgotten subscription is visible. Files: `src/feed.ts` 2. Expand recurrence correctly in the feed Including exceptions. External calendars are strict about this. Files: `src/feed.ts` 3. Complete export and deletion It is a family's data. Both directions should work fully. Files: `src/export.ts`, `src/deletion.ts` ### Done when - [ ] A feed subscribes and shows recurrence correctly - [ ] Revoking a feed stops it immediately - [ ] Export contains events, lists, meals and recipes - [ ] Deletion removes feeds and queued reminders ### Watch out - A calendar feed URL is a bearer credential exposing your family's whereabouts to anyone who has it, and those URLs end up in the sync settings of every device a person has ever used. Make each revocable independently and show when each was last fetched, so an unfamiliar one is visible.
# Operations · Cozi Gold ## Backup A family calendar with years of history is not reconstructible. Nightly backups with a tested restore, and be careful where they live, since this is location data about children. ## Restore Restore and confirm recurrence exceptions survived, since those are the easiest thing to lose and their loss reinstates cancelled events, which is worse than an outage. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Reminder delivery success and lateness, plus list operation conflicts. A rising conflict rate usually means a client is writing whole lists somewhere. ## Incident checklist If an event shows on the wrong day for one person, an all-day event is stored as an instant. If a household reports missing shopping items, a whole-list write has crept in. Both are data model defects rather than display bugs. 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 - [ ] An all-day event is confirmed to show identically across two zones - [ ] Recurrence is confirmed correct across both daylight transitions - [ ] Concurrent list additions are confirmed both to survive - [ ] Regenerating a shopping list is confirmed to preserve manual items - [ ] Revoking a calendar feed is confirmed to stop it immediately ## Launch constraint Do not market omitted Cozi Gold 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 · secret. A family's calendar and whereabouts. Treat it accordingly. DATABASE_URL=postgres://localhost:5432/household # Required · secret. Signs login sessions. Rotating it logs the household out, which is the intended emergency action. SESSION_SECRET=a random 32-byte hex string # Required. The household's zone. Individuals can differ, which is what phase 2 handles. DEFAULT_TIMEZONE=Europe/London # Required. When reminders defer. The default that stops the tool being muted in week one. QUIET_HOURS_DEFAULT=21:00-07:00 # Required. Where calendar feeds are served. The token in the path is the credential. FEED_BASE_URL=https://home.example.com/feed # Optional · secret. Phase 5 only. Absent means reminders are visible in the application but not delivered. SMTP_URL=smtp://user:pass@host:587
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Cozi Gold 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.
xfamily adoption, mobile apps, reminders, sync, and years of household workflow polish
xfrictionless family sync
xretailer and smart-home integrations
xlarge recipe or product database
xmobile polish
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Cozi Gold pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| cozi free | $0 | $0 | Shared calendar, lists and recipes for the family; no numeric caps disclosed. |
| cozi gold | — | $3.25 | Whole-family upgrade; no ads, mobile month view, calendar search, up to 3 reminders per event, change notifications, birthday tracker, shopping mode, premium support. |
free tierfree plan exists with shared calendar/lists/recipes; no numeric caps disclosed.
billingannual on public page ($39/year); no monthly plan published
hidden costsGold applies to everyone in the family; free tier is ad-supported.
verified 2026-08-10 · source ↗
Vibecode Cozi Gold
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Cozi Gold replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Cozi Gold cost?
Cozi Gold costs about $3.25/month (Gold, checked 2026-07-31), which is $39 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Cozi Gold?
Honestly: family adoption, mobile apps, reminders, sync, and years of household workflow polish; 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 Cozi Gold?
Yes: Cozi Free (The same shared calendar, lists, tasks, and recipes for $0; the ads and nicer controls stay upstairs.) Family Tools Basic (Calendar, chores, and shared lists for twelve people; meal planning is exactly where the paywall starts.) The prompt is for when you want it exactly your way.