Vibecode Height
track this build4 phases, 12 steps, beginner friendly0%🪦 Height shut down on September 24, 2025. The verdict below is now a post-mortem.
Height's solo core is compact: build a single-user AI project management workspace with projects, tasks, statuses, filters, recurring items, and JSON export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on collaboration, integrations, polish.
You are building a lean indie version of Height. 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 ===== # Height · indie build A personal project tracker whose hard problems are recurrence that behaves the way the calendar standard specifies, saved views that survive a schema change, and due dates that arrive on the day the person meant. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, single user, bound to the loopback interface | with no accounts, the network interface is the entire access control story | | Storage | SQLite with statuses and views stored as rows | changing a workflow should be data entry, not a deployment | | Recurrence | A documented subset of the calendar recurrence rule, including its skip semantics | the standard already answered the questions you are about to answer wrongly | | Dates | Date only fields stored as dates, timed fields as instants with a zone | a due date pushed through midnight in UTC arrives on the wrong day | | Views | Structured filter documents compiled to queries at read time | a saved string of SQL is a migration hazard and an injection surface at once | ## 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 application and the test suite. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A recurrence library, or the standard text open beside you** · free - Why: Hand rolling recurrence is where the skip rules get invented rather than followed, and the standard states them plainly. - Get it: A maintained recurrence library for your runtime, or the recurrence rule section of RFC 5545. - Verify: The monthly rule on the 31st example from the standard produces the dates the standard shows - [ ] **Your statuses and their categories** · free - Why: The categories are what the code branches on, so inventing them while writing the first list view guarantees a fourth category appears later. - Get it: Write down each status, its category of open, done or cancelled, and its order. - Verify: The list is in the seed data and every status maps to one of three categories - [ ] **A real backlog to import** · free - Why: Recurring tasks, odd due dates and half finished work only exist in a real list, and a seeded demo list will pass every test that matters. - Get it: Export your current task list to CSV, or copy fifty real tasks by hand. - Verify: It imports with the dates and the recurrence rules intact - [ ] **A test timezone far from UTC** · free - Why: Date handling looks perfect from a machine set to UTC, which is why the bug reaches everyone else. - Get it: Pick a zone at least eight hours from UTC with a daylight saving change and run the suite under it. - Verify: The suite passes with that zone set and the two transition dates included ## 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: - Multiple users and permissions. - Real time collaboration. - An integration catalogue. - Mobile capture and push notifications. - enterprise permissions - large integration catalog - mobile capture and notifications - real-time multiplayer If one of those is essential to you, that is the reason to keep paying for Height, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Height 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 single user project tracker whose recurring tasks follow the calendar standard instead of improvising to replace Height. 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: Next.js with TypeScript, single user, bound to the loopback interface · with no accounts, the network interface is the entire access control story - Storage: SQLite with statuses and views stored as rows · changing a workflow should be data entry, not a deployment - Recurrence: A documented subset of the calendar recurrence rule, including its skip semantics · the standard already answered the questions you are about to answer wrongly - Dates: Date only fields stored as dates, timed fields as instants with a zone · a due date pushed through midnight in UTC arrives on the wrong day - Views: Structured filter documents compiled to queries at read time · a saved string of SQL is a migration hazard and an injection surface at once ### Data model (create this before Phase 1) - projects(id, name, key, archived_at) - statuses(id, project_id, name, category, ordinal) · category is open, done or cancelled, and code branches on category and never on the name - tasks(id, project_id, title, body, status_id, priority, due_date, due_at, due_zone, created_at, completed_at, archived_at) - recurrences(task_id, rule_text, dtstart_date, dtstart_zone, count_remaining, until_date, materialised_to) · a documented subset of the recurrence rule - instances(id, recurrence_task_id, occurrence_date, task_id, skipped_reason) · materialised ahead, and skipped occurrences are recorded with why - views(id, name, filter_json, sort_json, group_by, ordinal) · a structured document compiled to a query, never stored SQL - activity(id, task_id, at, kind, detail) · the only record of what a task looked like before the last edit ### Phase 1 · Tasks, and a workflow that is data Build: Projects, tasks and statuses, where each status belongs to a project and carries a category of open, done or cancelled. Every behaviour keys off the category rather than the name. Deleting a status requires choosing where its tasks go. Trap: Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. Done when: Adding a status is done entirely in the interface with no code change, demonstrated. No code compares a status by name anywhere, verified by search. Deleting a status refuses until a destination is chosen, and the move is recorded in activity. A task always has exactly one status and the completion timestamp is set when it enters a done category. Do not build yet: No views, recurrence, quick entry or export. ### Phase 2 · Views that are saved queries Build: Store each view as a structured filter document with fields, operators and values, plus sort and grouping. Compile it to a parameterised query at read time. Show a view that references a missing field as broken with the field named, rather than returning nothing. Trap: Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. Done when: No stored view contains SQL, verified by inspecting the table. A view referencing a removed field renders as broken and names the field instead of showing zero results. Values are always bound as parameters, asserted by a test that passes a quote character through a filter. The same view produces the same rows through the interface and through the export. Do not build yet: No recurrence, quick entry or export yet. ### Phase 3 · Recurrence the way the standard defines it Build: Support a documented subset of the recurrence rule: frequency, interval, by day, by month day, a count and an until date, anchored to a start date and zone. Materialise occurrences ahead of time, record skipped ones with a reason, and preview the next ten dates before saving a rule. Trap: Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. Done when: A monthly rule on the 31st generates nothing in February and the skipped month does not consume the count, tested against the example in the standard. A rule whose occurrence lands on a nonexistent local time during a clock change is skipped rather than shifted, tested with both transition dates. The next ten occurrences are previewed before a rule can be saved. Completing one occurrence completes that occurrence only and the series continues. Do not build yet: No quick entry, archive or export yet. ### Phase 4 · Dates that arrive on the day you meant Build: Quick entry parses a title with an optional date phrase and shows what it understood before creating the task. Store a date only due date as a date and a timed one as an instant with its zone. Compute due today, overdue and this week in the user's zone. Trap: Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. Done when: A task due on a given date shows that same date for a user in a zone many hours from UTC, tested with two zones. Quick entry shows its interpretation before the task is created and the interpretation can be corrected. Overdue is computed against the user's zone and changes at their local midnight. A date only field never passes through an instant conversion, verified by type and by test. Do not build yet: No archive, export or backup yet. ### Phase 5 · Everything out, and everything back in Build: Export projects, statuses, views, tasks, recurrence rules and activity as JSON, and CSV for the task list. Import restores them into an empty database and reproduces the same views and the same next ten occurrences. Archive completed tasks. Keep the service on the loopback interface. Trap: Exporting the materialised task instances and not the recurrence rules produces an import that looks complete and never repeats again, and the loss shows up weeks later as work that quietly stopped appearing. Export the rules, and assert in the suite that the next ten occurrences match before and after a round trip. Done when: An export imported into an empty database reproduces identical views, statuses and the same next ten occurrences for every recurring task, tested. Archiving removes tasks from views without deleting them and they remain in the export. The application refuses to bind to a non loopback address without an explicit setting and a warning. A restore from the backup file is exercised in the suite rather than documented. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiple users and permissions, which turns every list query into an authorisation question and is most of what the paid product was selling. - Real time collaboration, which needs a merge model and a server rather than a single local database. - An integration catalogue, where each connector is a credential, a webhook and a schema that changes without warning. - Mobile capture and push notifications, which need a hosted component and an application store presence to be worth anything. ### README must contain - Which subset of the calendar recurrence rule is supported, named part by part - That invalid dates and nonexistent local times are skipped rather than shifted, and do not consume a count - That date only due dates are stored as dates and never converted through an instant - That views are structured documents compiled to parameterised queries, and never stored SQL - That the export includes recurrence rules, and how a round trip is verified ===== AGENTS.md ===== # Agent instructions · Height indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, single user, bound to the loopback interface, SQLite with statuses and views stored as rows, A documented subset of the calendar recurrence rule, including its skip semantics, Date only fields stored as dates, timed fields as instants with a zone, Structured filter documents compiled to queries at read time. 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 - Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. - Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. - Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. - Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. ===== BUILD_PLAN.md ===== # Build plan · Height A personal project tracker whose hard problems are recurrence that behaves the way the calendar standard specifies, saved views that survive a schema change, and due dates that arrive on the day the person meant. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Tasks, and a workflow that is data Statuses are rows with a category, so changing the workflow is not a migration. ### Steps 1. Give every status a category Three categories is enough, and it is what the code is allowed to see. Files: `src/db/schema.sql`, `src/statuses.ts` 2. Forbid comparing statuses by name A grep in the test suite is a fine way to enforce it. Files: `tests/no-status-name-checks.test.ts` 3. Make deletion require a destination Orphaned tasks disappear from lists and reappear as a mystery. Files: `src/statuses.ts` ### Done when - [ ] Statuses are added without a code change - [ ] No code compares a status by name - [ ] Deleting a status requires a destination - [ ] Entering a done category sets the completion time ### Watch out - Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. ## Phase 2 · Views that are saved queries A named filter keeps working after the schema changes. ### Steps 1. Store filters as structured documents Fields, operators and values, each validated against the schema. Files: `src/views/filter.ts` 2. Compile to parameterised queries Every value bound, every time, with a test that proves it. Files: `src/views/compile.ts`, `tests/filter-injection.test.ts` 3. Show broken views as broken An empty list looks like an answer and is not one. Files: `src/views/render.tsx` ### Done when - [ ] No view stores SQL - [ ] A missing field renders as broken and is named - [ ] Filter values are always bound parameters - [ ] Interface and export agree for one view ### Watch out - Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. ## Phase 3 · Recurrence the way the standard defines it A repeating task generates the right dates, including the ones that must not exist. ### Steps 1. Implement a documented subset, and say which Naming the parts you support is what keeps the behaviour honest. Files: `src/recurrence/parse.ts`, `docs/recurrence.md` 2. Skip invalid dates, and record the skip Not clamped, not shifted, and not counted toward the count. Files: `src/recurrence/expand.ts`, `tests/recurrence-invalid.test.ts` 3. Preview before saving a rule Ten dates on screen catches every misunderstanding in one second. Files: `src/recurrence/preview.tsx` ### Done when - [ ] A monthly rule on the 31st skips February without consuming the count - [ ] Nonexistent local times are skipped, both transitions tested - [ ] The next ten occurrences are previewed before saving - [ ] Completing one occurrence leaves the series running ### Watch out - Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. ## Phase 4 · Dates that arrive on the day you meant Quick entry produces the task and the date the person had in mind. ### Steps 1. Keep date only fields as dates Different type, different column, and never converted. Files: `src/dates.ts`, `tests/date-only.test.ts` 2. Show the parse before creating Quick entry that guesses silently creates the wrong task quickly. Files: `src/quickadd.tsx` 3. Compute today in the user's zone Midnight is a local event, and every list depends on it. Files: `src/dates.ts` ### Done when - [ ] A due date shows the same date in two distant zones - [ ] Quick entry shows and allows correcting its parse - [ ] Overdue changes at local midnight - [ ] Date only fields never become instants ### Watch out - Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. ## Not in this build - Multiple users and permissions. - Real time collaboration. - An integration catalogue. - Mobile capture and push notifications. ## After v1, if you want it - A board view over the same structured filters, which is a rendering change rather than a new query path - Dependencies between tasks, once statuses and categories have settled - A daily digest written to a file, which needs no notification infrastructure at all ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Projects, tasks, views and recurrence rules. DATABASE_PATH=./data/tracker.db # Required. The whole access control model. Changing it should require thinking. BIND_ADDRESS=127.0.0.1 # Required. Local port for the interface. PORT=4321 # Required. Used for today, overdue and this week. Set it to something other than UTC while testing. USER_TIMEZONE=Pacific/Auckland # Required. How far recurring occurrences are generated in advance. MATERIALISE_AHEAD_DAYS=90 # Required. Completed tasks leave views after this, and stay in exports. ARCHIVE_AFTER_DAYS=30 # Required. Where the JSON and CSV exports are written. EXPORT_DIR=./exports
You are building a lean indie version of Height. 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 ===== # Height · indie build A personal project tracker whose hard problems are recurrence that behaves the way the calendar standard specifies, saved views that survive a schema change, and due dates that arrive on the day the person meant. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, single user, bound to the loopback interface | with no accounts, the network interface is the entire access control story | | Storage | SQLite with statuses and views stored as rows | changing a workflow should be data entry, not a deployment | | Recurrence | A documented subset of the calendar recurrence rule, including its skip semantics | the standard already answered the questions you are about to answer wrongly | | Dates | Date only fields stored as dates, timed fields as instants with a zone | a due date pushed through midnight in UTC arrives on the wrong day | | Views | Structured filter documents compiled to queries at read time | a saved string of SQL is a migration hazard and an injection surface at once | ## 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 application and the test suite. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A recurrence library, or the standard text open beside you** · free - Why: Hand rolling recurrence is where the skip rules get invented rather than followed, and the standard states them plainly. - Get it: A maintained recurrence library for your runtime, or the recurrence rule section of RFC 5545. - Verify: The monthly rule on the 31st example from the standard produces the dates the standard shows - [ ] **Your statuses and their categories** · free - Why: The categories are what the code branches on, so inventing them while writing the first list view guarantees a fourth category appears later. - Get it: Write down each status, its category of open, done or cancelled, and its order. - Verify: The list is in the seed data and every status maps to one of three categories - [ ] **A real backlog to import** · free - Why: Recurring tasks, odd due dates and half finished work only exist in a real list, and a seeded demo list will pass every test that matters. - Get it: Export your current task list to CSV, or copy fifty real tasks by hand. - Verify: It imports with the dates and the recurrence rules intact - [ ] **A test timezone far from UTC** · free - Why: Date handling looks perfect from a machine set to UTC, which is why the bug reaches everyone else. - Get it: Pick a zone at least eight hours from UTC with a daylight saving change and run the suite under it. - Verify: The suite passes with that zone set and the two transition dates included ## 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: - Multiple users and permissions. - Real time collaboration. - An integration catalogue. - Mobile capture and push notifications. - enterprise permissions - large integration catalog - mobile capture and notifications - real-time multiplayer If one of those is essential to you, that is the reason to keep paying for Height, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Height 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 single user project tracker whose recurring tasks follow the calendar standard instead of improvising to replace Height. 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: Next.js with TypeScript, single user, bound to the loopback interface · with no accounts, the network interface is the entire access control story - Storage: SQLite with statuses and views stored as rows · changing a workflow should be data entry, not a deployment - Recurrence: A documented subset of the calendar recurrence rule, including its skip semantics · the standard already answered the questions you are about to answer wrongly - Dates: Date only fields stored as dates, timed fields as instants with a zone · a due date pushed through midnight in UTC arrives on the wrong day - Views: Structured filter documents compiled to queries at read time · a saved string of SQL is a migration hazard and an injection surface at once ### Data model (create this before Phase 1) - projects(id, name, key, archived_at) - statuses(id, project_id, name, category, ordinal) · category is open, done or cancelled, and code branches on category and never on the name - tasks(id, project_id, title, body, status_id, priority, due_date, due_at, due_zone, created_at, completed_at, archived_at) - recurrences(task_id, rule_text, dtstart_date, dtstart_zone, count_remaining, until_date, materialised_to) · a documented subset of the recurrence rule - instances(id, recurrence_task_id, occurrence_date, task_id, skipped_reason) · materialised ahead, and skipped occurrences are recorded with why - views(id, name, filter_json, sort_json, group_by, ordinal) · a structured document compiled to a query, never stored SQL - activity(id, task_id, at, kind, detail) · the only record of what a task looked like before the last edit ### Phase 1 · Tasks, and a workflow that is data Build: Projects, tasks and statuses, where each status belongs to a project and carries a category of open, done or cancelled. Every behaviour keys off the category rather than the name. Deleting a status requires choosing where its tasks go. Trap: Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. Done when: Adding a status is done entirely in the interface with no code change, demonstrated. No code compares a status by name anywhere, verified by search. Deleting a status refuses until a destination is chosen, and the move is recorded in activity. A task always has exactly one status and the completion timestamp is set when it enters a done category. Do not build yet: No views, recurrence, quick entry or export. ### Phase 2 · Views that are saved queries Build: Store each view as a structured filter document with fields, operators and values, plus sort and grouping. Compile it to a parameterised query at read time. Show a view that references a missing field as broken with the field named, rather than returning nothing. Trap: Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. Done when: No stored view contains SQL, verified by inspecting the table. A view referencing a removed field renders as broken and names the field instead of showing zero results. Values are always bound as parameters, asserted by a test that passes a quote character through a filter. The same view produces the same rows through the interface and through the export. Do not build yet: No recurrence, quick entry or export yet. ### Phase 3 · Recurrence the way the standard defines it Build: Support a documented subset of the recurrence rule: frequency, interval, by day, by month day, a count and an until date, anchored to a start date and zone. Materialise occurrences ahead of time, record skipped ones with a reason, and preview the next ten dates before saving a rule. Trap: Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. Done when: A monthly rule on the 31st generates nothing in February and the skipped month does not consume the count, tested against the example in the standard. A rule whose occurrence lands on a nonexistent local time during a clock change is skipped rather than shifted, tested with both transition dates. The next ten occurrences are previewed before a rule can be saved. Completing one occurrence completes that occurrence only and the series continues. Do not build yet: No quick entry, archive or export yet. ### Phase 4 · Dates that arrive on the day you meant Build: Quick entry parses a title with an optional date phrase and shows what it understood before creating the task. Store a date only due date as a date and a timed one as an instant with its zone. Compute due today, overdue and this week in the user's zone. Trap: Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. Done when: A task due on a given date shows that same date for a user in a zone many hours from UTC, tested with two zones. Quick entry shows its interpretation before the task is created and the interpretation can be corrected. Overdue is computed against the user's zone and changes at their local midnight. A date only field never passes through an instant conversion, verified by type and by test. Do not build yet: No archive, export or backup yet. ### Phase 5 · Everything out, and everything back in Build: Export projects, statuses, views, tasks, recurrence rules and activity as JSON, and CSV for the task list. Import restores them into an empty database and reproduces the same views and the same next ten occurrences. Archive completed tasks. Keep the service on the loopback interface. Trap: Exporting the materialised task instances and not the recurrence rules produces an import that looks complete and never repeats again, and the loss shows up weeks later as work that quietly stopped appearing. Export the rules, and assert in the suite that the next ten occurrences match before and after a round trip. Done when: An export imported into an empty database reproduces identical views, statuses and the same next ten occurrences for every recurring task, tested. Archiving removes tasks from views without deleting them and they remain in the export. The application refuses to bind to a non loopback address without an explicit setting and a warning. A restore from the backup file is exercised in the suite rather than documented. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiple users and permissions, which turns every list query into an authorisation question and is most of what the paid product was selling. - Real time collaboration, which needs a merge model and a server rather than a single local database. - An integration catalogue, where each connector is a credential, a webhook and a schema that changes without warning. - Mobile capture and push notifications, which need a hosted component and an application store presence to be worth anything. ### README must contain - Which subset of the calendar recurrence rule is supported, named part by part - That invalid dates and nonexistent local times are skipped rather than shifted, and do not consume a count - That date only due dates are stored as dates and never converted through an instant - That views are structured documents compiled to parameterised queries, and never stored SQL - That the export includes recurrence rules, and how a round trip is verified ===== AGENTS.md ===== # Agent instructions · Height indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, single user, bound to the loopback interface, SQLite with statuses and views stored as rows, A documented subset of the calendar recurrence rule, including its skip semantics, Date only fields stored as dates, timed fields as instants with a zone, Structured filter documents compiled to queries at read time. 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 - Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. - Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. - Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. - Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. ===== BUILD_PLAN.md ===== # Build plan · Height A personal project tracker whose hard problems are recurrence that behaves the way the calendar standard specifies, saved views that survive a schema change, and due dates that arrive on the day the person meant. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Tasks, and a workflow that is data Statuses are rows with a category, so changing the workflow is not a migration. ### Steps 1. Give every status a category Three categories is enough, and it is what the code is allowed to see. Files: `src/db/schema.sql`, `src/statuses.ts` 2. Forbid comparing statuses by name A grep in the test suite is a fine way to enforce it. Files: `tests/no-status-name-checks.test.ts` 3. Make deletion require a destination Orphaned tasks disappear from lists and reappear as a mystery. Files: `src/statuses.ts` ### Done when - [ ] Statuses are added without a code change - [ ] No code compares a status by name - [ ] Deleting a status requires a destination - [ ] Entering a done category sets the completion time ### Watch out - Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. ## Phase 2 · Views that are saved queries A named filter keeps working after the schema changes. ### Steps 1. Store filters as structured documents Fields, operators and values, each validated against the schema. Files: `src/views/filter.ts` 2. Compile to parameterised queries Every value bound, every time, with a test that proves it. Files: `src/views/compile.ts`, `tests/filter-injection.test.ts` 3. Show broken views as broken An empty list looks like an answer and is not one. Files: `src/views/render.tsx` ### Done when - [ ] No view stores SQL - [ ] A missing field renders as broken and is named - [ ] Filter values are always bound parameters - [ ] Interface and export agree for one view ### Watch out - Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. ## Phase 3 · Recurrence the way the standard defines it A repeating task generates the right dates, including the ones that must not exist. ### Steps 1. Implement a documented subset, and say which Naming the parts you support is what keeps the behaviour honest. Files: `src/recurrence/parse.ts`, `docs/recurrence.md` 2. Skip invalid dates, and record the skip Not clamped, not shifted, and not counted toward the count. Files: `src/recurrence/expand.ts`, `tests/recurrence-invalid.test.ts` 3. Preview before saving a rule Ten dates on screen catches every misunderstanding in one second. Files: `src/recurrence/preview.tsx` ### Done when - [ ] A monthly rule on the 31st skips February without consuming the count - [ ] Nonexistent local times are skipped, both transitions tested - [ ] The next ten occurrences are previewed before saving - [ ] Completing one occurrence leaves the series running ### Watch out - Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. ## Phase 4 · Dates that arrive on the day you meant Quick entry produces the task and the date the person had in mind. ### Steps 1. Keep date only fields as dates Different type, different column, and never converted. Files: `src/dates.ts`, `tests/date-only.test.ts` 2. Show the parse before creating Quick entry that guesses silently creates the wrong task quickly. Files: `src/quickadd.tsx` 3. Compute today in the user's zone Midnight is a local event, and every list depends on it. Files: `src/dates.ts` ### Done when - [ ] A due date shows the same date in two distant zones - [ ] Quick entry shows and allows correcting its parse - [ ] Overdue changes at local midnight - [ ] Date only fields never become instants ### Watch out - Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. ## Not in this build - Multiple users and permissions. - Real time collaboration. - An integration catalogue. - Mobile capture and push notifications. ## After v1, if you want it - A board view over the same structured filters, which is a rendering change rather than a new query path - Dependencies between tasks, once statuses and categories have settled - A daily digest written to a file, which needs no notification infrastructure at all ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Projects, tasks, views and recurrence rules. DATABASE_PATH=./data/tracker.db # Required. The whole access control model. Changing it should require thinking. BIND_ADDRESS=127.0.0.1 # Required. Local port for the interface. PORT=4321 # Required. Used for today, overdue and this week. Set it to something other than UTC while testing. USER_TIMEZONE=Pacific/Auckland # Required. How far recurring occurrences are generated in advance. MATERIALISE_AHEAD_DAYS=90 # Required. Completed tasks leave views after this, and stay in exports. ARCHIVE_AFTER_DAYS=30 # Required. Where the JSON and CSV exports are written. EXPORT_DIR=./exports
You are building a production product version of Height. 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 ===== # Height · product brief ## Problem Height's solo core is compact: build a single-user AI project management workspace with projects, tasks, statuses, filters, recurring items, and JSON export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on collaboration, integrations, polish. ## Product outcome A tracker where the workflow is data rather than code, saved views survive a schema change, recurring tasks skip the dates the standard says do not exist, and an export brings back the rules and not just the leftovers. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - local or self-hosted runtime - SQLite database ## Explicit non-goals for v1 - Multiple users and permissions. - Real time collaboration. - An integration catalogue. - Mobile capture and push notifications. - enterprise permissions - large integration catalog - mobile capture and notifications - real-time multiplayer ## Success criteria - No code is confirmed to compare a status by name - Views are confirmed free of stored SQL - The monthly 31st rule is confirmed to skip February without consuming the count - A due date is confirmed identical in two distant zones - A round trip is confirmed to reproduce the next occurrences ===== BRIEF.md ===== # Build brief · Height 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 single user project tracker whose recurring tasks follow the calendar standard instead of improvising to replace Height. 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: Next.js with TypeScript, single user, bound to the loopback interface · with no accounts, the network interface is the entire access control story - Storage: SQLite with statuses and views stored as rows · changing a workflow should be data entry, not a deployment - Recurrence: A documented subset of the calendar recurrence rule, including its skip semantics · the standard already answered the questions you are about to answer wrongly - Dates: Date only fields stored as dates, timed fields as instants with a zone · a due date pushed through midnight in UTC arrives on the wrong day - Views: Structured filter documents compiled to queries at read time · a saved string of SQL is a migration hazard and an injection surface at once ### Data model (create this before Phase 1) - projects(id, name, key, archived_at) - statuses(id, project_id, name, category, ordinal) · category is open, done or cancelled, and code branches on category and never on the name - tasks(id, project_id, title, body, status_id, priority, due_date, due_at, due_zone, created_at, completed_at, archived_at) - recurrences(task_id, rule_text, dtstart_date, dtstart_zone, count_remaining, until_date, materialised_to) · a documented subset of the recurrence rule - instances(id, recurrence_task_id, occurrence_date, task_id, skipped_reason) · materialised ahead, and skipped occurrences are recorded with why - views(id, name, filter_json, sort_json, group_by, ordinal) · a structured document compiled to a query, never stored SQL - activity(id, task_id, at, kind, detail) · the only record of what a task looked like before the last edit ### Phase 1 · Tasks, and a workflow that is data Build: Projects, tasks and statuses, where each status belongs to a project and carries a category of open, done or cancelled. Every behaviour keys off the category rather than the name. Deleting a status requires choosing where its tasks go. Trap: Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. Done when: Adding a status is done entirely in the interface with no code change, demonstrated. No code compares a status by name anywhere, verified by search. Deleting a status refuses until a destination is chosen, and the move is recorded in activity. A task always has exactly one status and the completion timestamp is set when it enters a done category. Do not build yet: No views, recurrence, quick entry or export. ### Phase 2 · Views that are saved queries Build: Store each view as a structured filter document with fields, operators and values, plus sort and grouping. Compile it to a parameterised query at read time. Show a view that references a missing field as broken with the field named, rather than returning nothing. Trap: Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. Done when: No stored view contains SQL, verified by inspecting the table. A view referencing a removed field renders as broken and names the field instead of showing zero results. Values are always bound as parameters, asserted by a test that passes a quote character through a filter. The same view produces the same rows through the interface and through the export. Do not build yet: No recurrence, quick entry or export yet. ### Phase 3 · Recurrence the way the standard defines it Build: Support a documented subset of the recurrence rule: frequency, interval, by day, by month day, a count and an until date, anchored to a start date and zone. Materialise occurrences ahead of time, record skipped ones with a reason, and preview the next ten dates before saving a rule. Trap: Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. Done when: A monthly rule on the 31st generates nothing in February and the skipped month does not consume the count, tested against the example in the standard. A rule whose occurrence lands on a nonexistent local time during a clock change is skipped rather than shifted, tested with both transition dates. The next ten occurrences are previewed before a rule can be saved. Completing one occurrence completes that occurrence only and the series continues. Do not build yet: No quick entry, archive or export yet. ### Phase 4 · Dates that arrive on the day you meant Build: Quick entry parses a title with an optional date phrase and shows what it understood before creating the task. Store a date only due date as a date and a timed one as an instant with its zone. Compute due today, overdue and this week in the user's zone. Trap: Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. Done when: A task due on a given date shows that same date for a user in a zone many hours from UTC, tested with two zones. Quick entry shows its interpretation before the task is created and the interpretation can be corrected. Overdue is computed against the user's zone and changes at their local midnight. A date only field never passes through an instant conversion, verified by type and by test. Do not build yet: No archive, export or backup yet. ### Phase 5 · Everything out, and everything back in Build: Export projects, statuses, views, tasks, recurrence rules and activity as JSON, and CSV for the task list. Import restores them into an empty database and reproduces the same views and the same next ten occurrences. Archive completed tasks. Keep the service on the loopback interface. Trap: Exporting the materialised task instances and not the recurrence rules produces an import that looks complete and never repeats again, and the loss shows up weeks later as work that quietly stopped appearing. Export the rules, and assert in the suite that the next ten occurrences match before and after a round trip. Done when: An export imported into an empty database reproduces identical views, statuses and the same next ten occurrences for every recurring task, tested. Archiving removes tasks from views without deleting them and they remain in the export. The application refuses to bind to a non loopback address without an explicit setting and a warning. A restore from the backup file is exercised in the suite rather than documented. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiple users and permissions, which turns every list query into an authorisation question and is most of what the paid product was selling. - Real time collaboration, which needs a merge model and a server rather than a single local database. - An integration catalogue, where each connector is a credential, a webhook and a schema that changes without warning. - Mobile capture and push notifications, which need a hosted component and an application store presence to be worth anything. ### README must contain - Which subset of the calendar recurrence rule is supported, named part by part - That invalid dates and nonexistent local times are skipped rather than shifted, and do not consume a count - That date only due dates are stored as dates and never converted through an instant - That views are structured documents compiled to parameterised queries, and never stored SQL - That the export includes recurrence rules, and how a round trip is verified ===== ARCHITECTURE.md ===== # Architecture · Height ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, single user, bound to the loopback interface | with no accounts, the network interface is the entire access control story | | Storage | SQLite with statuses and views stored as rows | changing a workflow should be data entry, not a deployment | | Recurrence | A documented subset of the calendar recurrence rule, including its skip semantics | the standard already answered the questions you are about to answer wrongly | | Dates | Date only fields stored as dates, timed fields as instants with a zone | a due date pushed through midnight in UTC arrives on the wrong day | | Views | Structured filter documents compiled to queries at read time | a saved string of SQL is a migration hazard and an injection surface at once | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Workflow | Statuses and their categories. | Any set of statuses, provided code branches on category only. | | Views | Structured filters compiled to queries. | Not swappable for stored SQL strings. | | Recurrence | Rule parsing, expansion and skip records. | Any library that follows the standard's skip semantics. | | Dates | Date only and instant fields kept apart. | Any date library, provided date only never becomes an instant. | | Portability | Export, import and archive. | Any format whose round trip reproduces the next occurrences. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Projects, tasks, views and recurrence rules. - `BIND_ADDRESS` · required · The whole access control model. Changing it should require thinking. - `PORT` · required · Local port for the interface. - `USER_TIMEZONE` · required · Used for today, overdue and this week. Set it to something other than UTC while testing. - `MATERIALISE_AHEAD_DAYS` · required · How far recurring occurrences are generated in advance. - `ARCHIVE_AFTER_DAYS` · required · Completed tasks leave views after this, and stay in exports. - `EXPORT_DIR` · required · Where the JSON and CSV exports are written. ## 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 · Height product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js with TypeScript, single user, bound to the loopback interface, SQLite with statuses and views stored as rows, A documented subset of the calendar recurrence rule, including its skip semantics, Date only fields stored as dates, timed fields as instants with a zone, Structured filter documents compiled to queries at read time. - 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 - Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. - Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. - Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. - Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. - Exporting the materialised task instances and not the recurrence rules produces an import that looks complete and never repeats again, and the loss shows up weeks later as work that quietly stopped appearing. Export the rules, and assert in the suite that the next ten occurrences match before and after a round trip. ===== MILESTONES.md ===== # Delivery milestones · Height Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Tasks, and a workflow that is data Statuses are rows with a category, so changing the workflow is not a migration. ### Steps 1. Give every status a category Three categories is enough, and it is what the code is allowed to see. Files: `src/db/schema.sql`, `src/statuses.ts` 2. Forbid comparing statuses by name A grep in the test suite is a fine way to enforce it. Files: `tests/no-status-name-checks.test.ts` 3. Make deletion require a destination Orphaned tasks disappear from lists and reappear as a mystery. Files: `src/statuses.ts` ### Done when - [ ] Statuses are added without a code change - [ ] No code compares a status by name - [ ] Deleting a status requires a destination - [ ] Entering a done category sets the completion time ### Watch out - Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. ## M2 · Views that are saved queries A named filter keeps working after the schema changes. ### Steps 1. Store filters as structured documents Fields, operators and values, each validated against the schema. Files: `src/views/filter.ts` 2. Compile to parameterised queries Every value bound, every time, with a test that proves it. Files: `src/views/compile.ts`, `tests/filter-injection.test.ts` 3. Show broken views as broken An empty list looks like an answer and is not one. Files: `src/views/render.tsx` ### Done when - [ ] No view stores SQL - [ ] A missing field renders as broken and is named - [ ] Filter values are always bound parameters - [ ] Interface and export agree for one view ### Watch out - Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. ## M3 · Recurrence the way the standard defines it A repeating task generates the right dates, including the ones that must not exist. ### Steps 1. Implement a documented subset, and say which Naming the parts you support is what keeps the behaviour honest. Files: `src/recurrence/parse.ts`, `docs/recurrence.md` 2. Skip invalid dates, and record the skip Not clamped, not shifted, and not counted toward the count. Files: `src/recurrence/expand.ts`, `tests/recurrence-invalid.test.ts` 3. Preview before saving a rule Ten dates on screen catches every misunderstanding in one second. Files: `src/recurrence/preview.tsx` ### Done when - [ ] A monthly rule on the 31st skips February without consuming the count - [ ] Nonexistent local times are skipped, both transitions tested - [ ] The next ten occurrences are previewed before saving - [ ] Completing one occurrence leaves the series running ### Watch out - Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. ## M4 · Dates that arrive on the day you meant Quick entry produces the task and the date the person had in mind. ### Steps 1. Keep date only fields as dates Different type, different column, and never converted. Files: `src/dates.ts`, `tests/date-only.test.ts` 2. Show the parse before creating Quick entry that guesses silently creates the wrong task quickly. Files: `src/quickadd.tsx` 3. Compute today in the user's zone Midnight is a local event, and every list depends on it. Files: `src/dates.ts` ### Done when - [ ] A due date shows the same date in two distant zones - [ ] Quick entry shows and allows correcting its parse - [ ] Overdue changes at local midnight - [ ] Date only fields never become instants ### Watch out - Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. ## M5 · Everything out, and everything back in (production only) An export round trips, including the rules that generate future work. ### Steps 1. Export rules, not just instances The rule is the part that makes future work happen. Files: `src/export.ts`, `tests/roundtrip.test.ts` 2. Import into an empty database and compare Same views, same statuses, same next ten dates. Files: `src/import.ts` 3. Keep the bind address explicit With no accounts, the interface it listens on is the whole boundary. Files: `src/server.ts` ### Done when - [ ] A round trip reproduces views and next occurrences - [ ] Archived tasks leave views but stay in exports - [ ] Binding beyond loopback requires an explicit setting - [ ] Restore is exercised in the suite ### Watch out - Exporting the materialised task instances and not the recurrence rules produces an import that looks complete and never repeats again, and the loss shows up weeks later as work that quietly stopped appearing. Export the rules, and assert in the suite that the next ten occurrences match before and after a round trip. ===== OPERATIONS.md ===== # Operations · Height ## Backup The SQLite file plus the latest JSON export, because the export is the copy you can read when a schema change makes the database awkward. Take one before every upgrade. ## Restore Restore, run the round trip test, and check that the next ten occurrences for every recurring task match what the export recorded. A mismatch means the recurrence expansion changed, not that the backup was bad. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Recurring tasks whose materialised horizon has stopped advancing, views that reference a missing field, and any task sitting in a status with no category. All three are queries you can run from the interface. ## Incident checklist The incident here is work that stopped appearing, which is almost always a recurrence rule that hit its until date, consumed its count, or was lost in an import that only carried instances. 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 - [ ] No code is confirmed to compare a status by name - [ ] Views are confirmed free of stored SQL - [ ] The monthly 31st rule is confirmed to skip February without consuming the count - [ ] A due date is confirmed identical in two distant zones - [ ] A round trip is confirmed to reproduce the next occurrences ## Launch constraint Do not market omitted Height 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. Projects, tasks, views and recurrence rules. DATABASE_PATH=./data/tracker.db # Required. The whole access control model. Changing it should require thinking. BIND_ADDRESS=127.0.0.1 # Required. Local port for the interface. PORT=4321 # Required. Used for today, overdue and this week. Set it to something other than UTC while testing. USER_TIMEZONE=Pacific/Auckland # Required. How far recurring occurrences are generated in advance. MATERIALISE_AHEAD_DAYS=90 # Required. Completed tasks leave views after this, and stay in exports. ARCHIVE_AFTER_DAYS=30 # Required. Where the JSON and CSV exports are written. EXPORT_DIR=./exports
# Height · indie build A personal project tracker whose hard problems are recurrence that behaves the way the calendar standard specifies, saved views that survive a schema change, and due dates that arrive on the day the person meant. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, single user, bound to the loopback interface | with no accounts, the network interface is the entire access control story | | Storage | SQLite with statuses and views stored as rows | changing a workflow should be data entry, not a deployment | | Recurrence | A documented subset of the calendar recurrence rule, including its skip semantics | the standard already answered the questions you are about to answer wrongly | | Dates | Date only fields stored as dates, timed fields as instants with a zone | a due date pushed through midnight in UTC arrives on the wrong day | | Views | Structured filter documents compiled to queries at read time | a saved string of SQL is a migration hazard and an injection surface at once | ## 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 application and the test suite. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A recurrence library, or the standard text open beside you** · free - Why: Hand rolling recurrence is where the skip rules get invented rather than followed, and the standard states them plainly. - Get it: A maintained recurrence library for your runtime, or the recurrence rule section of RFC 5545. - Verify: The monthly rule on the 31st example from the standard produces the dates the standard shows - [ ] **Your statuses and their categories** · free - Why: The categories are what the code branches on, so inventing them while writing the first list view guarantees a fourth category appears later. - Get it: Write down each status, its category of open, done or cancelled, and its order. - Verify: The list is in the seed data and every status maps to one of three categories - [ ] **A real backlog to import** · free - Why: Recurring tasks, odd due dates and half finished work only exist in a real list, and a seeded demo list will pass every test that matters. - Get it: Export your current task list to CSV, or copy fifty real tasks by hand. - Verify: It imports with the dates and the recurrence rules intact - [ ] **A test timezone far from UTC** · free - Why: Date handling looks perfect from a machine set to UTC, which is why the bug reaches everyone else. - Get it: Pick a zone at least eight hours from UTC with a daylight saving change and run the suite under it. - Verify: The suite passes with that zone set and the two transition dates included ## 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: - Multiple users and permissions. - Real time collaboration. - An integration catalogue. - Mobile capture and push notifications. - enterprise permissions - large integration catalog - mobile capture and notifications - real-time multiplayer If one of those is essential to you, that is the reason to keep paying for Height, and the README should say so rather than pretend.
# Build brief · Height 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 single user project tracker whose recurring tasks follow the calendar standard instead of improvising to replace Height. 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: Next.js with TypeScript, single user, bound to the loopback interface · with no accounts, the network interface is the entire access control story - Storage: SQLite with statuses and views stored as rows · changing a workflow should be data entry, not a deployment - Recurrence: A documented subset of the calendar recurrence rule, including its skip semantics · the standard already answered the questions you are about to answer wrongly - Dates: Date only fields stored as dates, timed fields as instants with a zone · a due date pushed through midnight in UTC arrives on the wrong day - Views: Structured filter documents compiled to queries at read time · a saved string of SQL is a migration hazard and an injection surface at once ### Data model (create this before Phase 1) - projects(id, name, key, archived_at) - statuses(id, project_id, name, category, ordinal) · category is open, done or cancelled, and code branches on category and never on the name - tasks(id, project_id, title, body, status_id, priority, due_date, due_at, due_zone, created_at, completed_at, archived_at) - recurrences(task_id, rule_text, dtstart_date, dtstart_zone, count_remaining, until_date, materialised_to) · a documented subset of the recurrence rule - instances(id, recurrence_task_id, occurrence_date, task_id, skipped_reason) · materialised ahead, and skipped occurrences are recorded with why - views(id, name, filter_json, sort_json, group_by, ordinal) · a structured document compiled to a query, never stored SQL - activity(id, task_id, at, kind, detail) · the only record of what a task looked like before the last edit ### Phase 1 · Tasks, and a workflow that is data Build: Projects, tasks and statuses, where each status belongs to a project and carries a category of open, done or cancelled. Every behaviour keys off the category rather than the name. Deleting a status requires choosing where its tasks go. Trap: Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. Done when: Adding a status is done entirely in the interface with no code change, demonstrated. No code compares a status by name anywhere, verified by search. Deleting a status refuses until a destination is chosen, and the move is recorded in activity. A task always has exactly one status and the completion timestamp is set when it enters a done category. Do not build yet: No views, recurrence, quick entry or export. ### Phase 2 · Views that are saved queries Build: Store each view as a structured filter document with fields, operators and values, plus sort and grouping. Compile it to a parameterised query at read time. Show a view that references a missing field as broken with the field named, rather than returning nothing. Trap: Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. Done when: No stored view contains SQL, verified by inspecting the table. A view referencing a removed field renders as broken and names the field instead of showing zero results. Values are always bound as parameters, asserted by a test that passes a quote character through a filter. The same view produces the same rows through the interface and through the export. Do not build yet: No recurrence, quick entry or export yet. ### Phase 3 · Recurrence the way the standard defines it Build: Support a documented subset of the recurrence rule: frequency, interval, by day, by month day, a count and an until date, anchored to a start date and zone. Materialise occurrences ahead of time, record skipped ones with a reason, and preview the next ten dates before saving a rule. Trap: Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. Done when: A monthly rule on the 31st generates nothing in February and the skipped month does not consume the count, tested against the example in the standard. A rule whose occurrence lands on a nonexistent local time during a clock change is skipped rather than shifted, tested with both transition dates. The next ten occurrences are previewed before a rule can be saved. Completing one occurrence completes that occurrence only and the series continues. Do not build yet: No quick entry, archive or export yet. ### Phase 4 · Dates that arrive on the day you meant Build: Quick entry parses a title with an optional date phrase and shows what it understood before creating the task. Store a date only due date as a date and a timed one as an instant with its zone. Compute due today, overdue and this week in the user's zone. Trap: Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. Done when: A task due on a given date shows that same date for a user in a zone many hours from UTC, tested with two zones. Quick entry shows its interpretation before the task is created and the interpretation can be corrected. Overdue is computed against the user's zone and changes at their local midnight. A date only field never passes through an instant conversion, verified by type and by test. Do not build yet: No archive, export or backup yet. ### Phase 5 · Everything out, and everything back in Build: Export projects, statuses, views, tasks, recurrence rules and activity as JSON, and CSV for the task list. Import restores them into an empty database and reproduces the same views and the same next ten occurrences. Archive completed tasks. Keep the service on the loopback interface. Trap: Exporting the materialised task instances and not the recurrence rules produces an import that looks complete and never repeats again, and the loss shows up weeks later as work that quietly stopped appearing. Export the rules, and assert in the suite that the next ten occurrences match before and after a round trip. Done when: An export imported into an empty database reproduces identical views, statuses and the same next ten occurrences for every recurring task, tested. Archiving removes tasks from views without deleting them and they remain in the export. The application refuses to bind to a non loopback address without an explicit setting and a warning. A restore from the backup file is exercised in the suite rather than documented. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiple users and permissions, which turns every list query into an authorisation question and is most of what the paid product was selling. - Real time collaboration, which needs a merge model and a server rather than a single local database. - An integration catalogue, where each connector is a credential, a webhook and a schema that changes without warning. - Mobile capture and push notifications, which need a hosted component and an application store presence to be worth anything. ### README must contain - Which subset of the calendar recurrence rule is supported, named part by part - That invalid dates and nonexistent local times are skipped rather than shifted, and do not consume a count - That date only due dates are stored as dates and never converted through an instant - That views are structured documents compiled to parameterised queries, and never stored SQL - That the export includes recurrence rules, and how a round trip is verified
# Agent instructions · Height indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, single user, bound to the loopback interface, SQLite with statuses and views stored as rows, A documented subset of the calendar recurrence rule, including its skip semantics, Date only fields stored as dates, timed fields as instants with a zone, Structured filter documents compiled to queries at read time. 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 - Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. - Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. - Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. - Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion.
# Build plan · Height A personal project tracker whose hard problems are recurrence that behaves the way the calendar standard specifies, saved views that survive a schema change, and due dates that arrive on the day the person meant. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Tasks, and a workflow that is data Statuses are rows with a category, so changing the workflow is not a migration. ### Steps 1. Give every status a category Three categories is enough, and it is what the code is allowed to see. Files: `src/db/schema.sql`, `src/statuses.ts` 2. Forbid comparing statuses by name A grep in the test suite is a fine way to enforce it. Files: `tests/no-status-name-checks.test.ts` 3. Make deletion require a destination Orphaned tasks disappear from lists and reappear as a mystery. Files: `src/statuses.ts` ### Done when - [ ] Statuses are added without a code change - [ ] No code compares a status by name - [ ] Deleting a status requires a destination - [ ] Entering a done category sets the completion time ### Watch out - Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. ## Phase 2 · Views that are saved queries A named filter keeps working after the schema changes. ### Steps 1. Store filters as structured documents Fields, operators and values, each validated against the schema. Files: `src/views/filter.ts` 2. Compile to parameterised queries Every value bound, every time, with a test that proves it. Files: `src/views/compile.ts`, `tests/filter-injection.test.ts` 3. Show broken views as broken An empty list looks like an answer and is not one. Files: `src/views/render.tsx` ### Done when - [ ] No view stores SQL - [ ] A missing field renders as broken and is named - [ ] Filter values are always bound parameters - [ ] Interface and export agree for one view ### Watch out - Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. ## Phase 3 · Recurrence the way the standard defines it A repeating task generates the right dates, including the ones that must not exist. ### Steps 1. Implement a documented subset, and say which Naming the parts you support is what keeps the behaviour honest. Files: `src/recurrence/parse.ts`, `docs/recurrence.md` 2. Skip invalid dates, and record the skip Not clamped, not shifted, and not counted toward the count. Files: `src/recurrence/expand.ts`, `tests/recurrence-invalid.test.ts` 3. Preview before saving a rule Ten dates on screen catches every misunderstanding in one second. Files: `src/recurrence/preview.tsx` ### Done when - [ ] A monthly rule on the 31st skips February without consuming the count - [ ] Nonexistent local times are skipped, both transitions tested - [ ] The next ten occurrences are previewed before saving - [ ] Completing one occurrence leaves the series running ### Watch out - Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. ## Phase 4 · Dates that arrive on the day you meant Quick entry produces the task and the date the person had in mind. ### Steps 1. Keep date only fields as dates Different type, different column, and never converted. Files: `src/dates.ts`, `tests/date-only.test.ts` 2. Show the parse before creating Quick entry that guesses silently creates the wrong task quickly. Files: `src/quickadd.tsx` 3. Compute today in the user's zone Midnight is a local event, and every list depends on it. Files: `src/dates.ts` ### Done when - [ ] A due date shows the same date in two distant zones - [ ] Quick entry shows and allows correcting its parse - [ ] Overdue changes at local midnight - [ ] Date only fields never become instants ### Watch out - Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. ## Not in this build - Multiple users and permissions. - Real time collaboration. - An integration catalogue. - Mobile capture and push notifications. ## After v1, if you want it - A board view over the same structured filters, which is a rendering change rather than a new query path - Dependencies between tasks, once statuses and categories have settled - A daily digest written to a file, which needs no notification infrastructure at all
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Projects, tasks, views and recurrence rules. DATABASE_PATH=./data/tracker.db # Required. The whole access control model. Changing it should require thinking. BIND_ADDRESS=127.0.0.1 # Required. Local port for the interface. PORT=4321 # Required. Used for today, overdue and this week. Set it to something other than UTC while testing. USER_TIMEZONE=Pacific/Auckland # Required. How far recurring occurrences are generated in advance. MATERIALISE_AHEAD_DAYS=90 # Required. Completed tasks leave views after this, and stay in exports. ARCHIVE_AFTER_DAYS=30 # Required. Where the JSON and CSV exports are written. EXPORT_DIR=./exports
# Height · product brief ## Problem Height's solo core is compact: build a single-user AI project management workspace with projects, tasks, statuses, filters, recurring items, and JSON export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on collaboration, integrations, polish. ## Product outcome A tracker where the workflow is data rather than code, saved views survive a schema change, recurring tasks skip the dates the standard says do not exist, and an export brings back the rules and not just the leftovers. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - local or self-hosted runtime - SQLite database ## Explicit non-goals for v1 - Multiple users and permissions. - Real time collaboration. - An integration catalogue. - Mobile capture and push notifications. - enterprise permissions - large integration catalog - mobile capture and notifications - real-time multiplayer ## Success criteria - No code is confirmed to compare a status by name - Views are confirmed free of stored SQL - The monthly 31st rule is confirmed to skip February without consuming the count - A due date is confirmed identical in two distant zones - A round trip is confirmed to reproduce the next occurrences
# Build brief · Height 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 single user project tracker whose recurring tasks follow the calendar standard instead of improvising to replace Height. 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: Next.js with TypeScript, single user, bound to the loopback interface · with no accounts, the network interface is the entire access control story - Storage: SQLite with statuses and views stored as rows · changing a workflow should be data entry, not a deployment - Recurrence: A documented subset of the calendar recurrence rule, including its skip semantics · the standard already answered the questions you are about to answer wrongly - Dates: Date only fields stored as dates, timed fields as instants with a zone · a due date pushed through midnight in UTC arrives on the wrong day - Views: Structured filter documents compiled to queries at read time · a saved string of SQL is a migration hazard and an injection surface at once ### Data model (create this before Phase 1) - projects(id, name, key, archived_at) - statuses(id, project_id, name, category, ordinal) · category is open, done or cancelled, and code branches on category and never on the name - tasks(id, project_id, title, body, status_id, priority, due_date, due_at, due_zone, created_at, completed_at, archived_at) - recurrences(task_id, rule_text, dtstart_date, dtstart_zone, count_remaining, until_date, materialised_to) · a documented subset of the recurrence rule - instances(id, recurrence_task_id, occurrence_date, task_id, skipped_reason) · materialised ahead, and skipped occurrences are recorded with why - views(id, name, filter_json, sort_json, group_by, ordinal) · a structured document compiled to a query, never stored SQL - activity(id, task_id, at, kind, detail) · the only record of what a task looked like before the last edit ### Phase 1 · Tasks, and a workflow that is data Build: Projects, tasks and statuses, where each status belongs to a project and carries a category of open, done or cancelled. Every behaviour keys off the category rather than the name. Deleting a status requires choosing where its tasks go. Trap: Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. Done when: Adding a status is done entirely in the interface with no code change, demonstrated. No code compares a status by name anywhere, verified by search. Deleting a status refuses until a destination is chosen, and the move is recorded in activity. A task always has exactly one status and the completion timestamp is set when it enters a done category. Do not build yet: No views, recurrence, quick entry or export. ### Phase 2 · Views that are saved queries Build: Store each view as a structured filter document with fields, operators and values, plus sort and grouping. Compile it to a parameterised query at read time. Show a view that references a missing field as broken with the field named, rather than returning nothing. Trap: Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. Done when: No stored view contains SQL, verified by inspecting the table. A view referencing a removed field renders as broken and names the field instead of showing zero results. Values are always bound as parameters, asserted by a test that passes a quote character through a filter. The same view produces the same rows through the interface and through the export. Do not build yet: No recurrence, quick entry or export yet. ### Phase 3 · Recurrence the way the standard defines it Build: Support a documented subset of the recurrence rule: frequency, interval, by day, by month day, a count and an until date, anchored to a start date and zone. Materialise occurrences ahead of time, record skipped ones with a reason, and preview the next ten dates before saving a rule. Trap: Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. Done when: A monthly rule on the 31st generates nothing in February and the skipped month does not consume the count, tested against the example in the standard. A rule whose occurrence lands on a nonexistent local time during a clock change is skipped rather than shifted, tested with both transition dates. The next ten occurrences are previewed before a rule can be saved. Completing one occurrence completes that occurrence only and the series continues. Do not build yet: No quick entry, archive or export yet. ### Phase 4 · Dates that arrive on the day you meant Build: Quick entry parses a title with an optional date phrase and shows what it understood before creating the task. Store a date only due date as a date and a timed one as an instant with its zone. Compute due today, overdue and this week in the user's zone. Trap: Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. Done when: A task due on a given date shows that same date for a user in a zone many hours from UTC, tested with two zones. Quick entry shows its interpretation before the task is created and the interpretation can be corrected. Overdue is computed against the user's zone and changes at their local midnight. A date only field never passes through an instant conversion, verified by type and by test. Do not build yet: No archive, export or backup yet. ### Phase 5 · Everything out, and everything back in Build: Export projects, statuses, views, tasks, recurrence rules and activity as JSON, and CSV for the task list. Import restores them into an empty database and reproduces the same views and the same next ten occurrences. Archive completed tasks. Keep the service on the loopback interface. Trap: Exporting the materialised task instances and not the recurrence rules produces an import that looks complete and never repeats again, and the loss shows up weeks later as work that quietly stopped appearing. Export the rules, and assert in the suite that the next ten occurrences match before and after a round trip. Done when: An export imported into an empty database reproduces identical views, statuses and the same next ten occurrences for every recurring task, tested. Archiving removes tasks from views without deleting them and they remain in the export. The application refuses to bind to a non loopback address without an explicit setting and a warning. A restore from the backup file is exercised in the suite rather than documented. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiple users and permissions, which turns every list query into an authorisation question and is most of what the paid product was selling. - Real time collaboration, which needs a merge model and a server rather than a single local database. - An integration catalogue, where each connector is a credential, a webhook and a schema that changes without warning. - Mobile capture and push notifications, which need a hosted component and an application store presence to be worth anything. ### README must contain - Which subset of the calendar recurrence rule is supported, named part by part - That invalid dates and nonexistent local times are skipped rather than shifted, and do not consume a count - That date only due dates are stored as dates and never converted through an instant - That views are structured documents compiled to parameterised queries, and never stored SQL - That the export includes recurrence rules, and how a round trip is verified
# Architecture · Height ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, single user, bound to the loopback interface | with no accounts, the network interface is the entire access control story | | Storage | SQLite with statuses and views stored as rows | changing a workflow should be data entry, not a deployment | | Recurrence | A documented subset of the calendar recurrence rule, including its skip semantics | the standard already answered the questions you are about to answer wrongly | | Dates | Date only fields stored as dates, timed fields as instants with a zone | a due date pushed through midnight in UTC arrives on the wrong day | | Views | Structured filter documents compiled to queries at read time | a saved string of SQL is a migration hazard and an injection surface at once | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Workflow | Statuses and their categories. | Any set of statuses, provided code branches on category only. | | Views | Structured filters compiled to queries. | Not swappable for stored SQL strings. | | Recurrence | Rule parsing, expansion and skip records. | Any library that follows the standard's skip semantics. | | Dates | Date only and instant fields kept apart. | Any date library, provided date only never becomes an instant. | | Portability | Export, import and archive. | Any format whose round trip reproduces the next occurrences. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Projects, tasks, views and recurrence rules. - `BIND_ADDRESS` · required · The whole access control model. Changing it should require thinking. - `PORT` · required · Local port for the interface. - `USER_TIMEZONE` · required · Used for today, overdue and this week. Set it to something other than UTC while testing. - `MATERIALISE_AHEAD_DAYS` · required · How far recurring occurrences are generated in advance. - `ARCHIVE_AFTER_DAYS` · required · Completed tasks leave views after this, and stay in exports. - `EXPORT_DIR` · required · Where the JSON and CSV exports are written. ## 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 · Height product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js with TypeScript, single user, bound to the loopback interface, SQLite with statuses and views stored as rows, A documented subset of the calendar recurrence rule, including its skip semantics, Date only fields stored as dates, timed fields as instants with a zone, Structured filter documents compiled to queries at read time. - 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 - Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. - Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. - Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. - Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. - Exporting the materialised task instances and not the recurrence rules produces an import that looks complete and never repeats again, and the loss shows up weeks later as work that quietly stopped appearing. Export the rules, and assert in the suite that the next ten occurrences match before and after a round trip.
# Delivery milestones · Height Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Tasks, and a workflow that is data Statuses are rows with a category, so changing the workflow is not a migration. ### Steps 1. Give every status a category Three categories is enough, and it is what the code is allowed to see. Files: `src/db/schema.sql`, `src/statuses.ts` 2. Forbid comparing statuses by name A grep in the test suite is a fine way to enforce it. Files: `tests/no-status-name-checks.test.ts` 3. Make deletion require a destination Orphaned tasks disappear from lists and reappear as a mystery. Files: `src/statuses.ts` ### Done when - [ ] Statuses are added without a code change - [ ] No code compares a status by name - [ ] Deleting a status requires a destination - [ ] Entering a done category sets the completion time ### Watch out - Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move. ## M2 · Views that are saved queries A named filter keeps working after the schema changes. ### Steps 1. Store filters as structured documents Fields, operators and values, each validated against the schema. Files: `src/views/filter.ts` 2. Compile to parameterised queries Every value bound, every time, with a test that proves it. Files: `src/views/compile.ts`, `tests/filter-injection.test.ts` 3. Show broken views as broken An empty list looks like an answer and is not one. Files: `src/views/render.tsx` ### Done when - [ ] No view stores SQL - [ ] A missing field renders as broken and is named - [ ] Filter values are always bound parameters - [ ] Interface and export agree for one view ### Watch out - Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time. ## M3 · Recurrence the way the standard defines it A repeating task generates the right dates, including the ones that must not exist. ### Steps 1. Implement a documented subset, and say which Naming the parts you support is what keeps the behaviour honest. Files: `src/recurrence/parse.ts`, `docs/recurrence.md` 2. Skip invalid dates, and record the skip Not clamped, not shifted, and not counted toward the count. Files: `src/recurrence/expand.ts`, `tests/recurrence-invalid.test.ts` 3. Preview before saving a rule Ten dates on screen catches every misunderstanding in one second. Files: `src/recurrence/preview.tsx` ### Done when - [ ] A monthly rule on the 31st skips February without consuming the count - [ ] Nonexistent local times are skipped, both transitions tested - [ ] The next ten occurrences are previewed before saving - [ ] Completing one occurrence leaves the series running ### Watch out - Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved. ## M4 · Dates that arrive on the day you meant Quick entry produces the task and the date the person had in mind. ### Steps 1. Keep date only fields as dates Different type, different column, and never converted. Files: `src/dates.ts`, `tests/date-only.test.ts` 2. Show the parse before creating Quick entry that guesses silently creates the wrong task quickly. Files: `src/quickadd.tsx` 3. Compute today in the user's zone Midnight is a local event, and every list depends on it. Files: `src/dates.ts` ### Done when - [ ] A due date shows the same date in two distant zones - [ ] Quick entry shows and allows correcting its parse - [ ] Overdue changes at local midnight - [ ] Date only fields never become instants ### Watch out - Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion. ## M5 · Everything out, and everything back in (production only) An export round trips, including the rules that generate future work. ### Steps 1. Export rules, not just instances The rule is the part that makes future work happen. Files: `src/export.ts`, `tests/roundtrip.test.ts` 2. Import into an empty database and compare Same views, same statuses, same next ten dates. Files: `src/import.ts` 3. Keep the bind address explicit With no accounts, the interface it listens on is the whole boundary. Files: `src/server.ts` ### Done when - [ ] A round trip reproduces views and next occurrences - [ ] Archived tasks leave views but stay in exports - [ ] Binding beyond loopback requires an explicit setting - [ ] Restore is exercised in the suite ### Watch out - Exporting the materialised task instances and not the recurrence rules produces an import that looks complete and never repeats again, and the loss shows up weeks later as work that quietly stopped appearing. Export the rules, and assert in the suite that the next ten occurrences match before and after a round trip.
# Operations · Height ## Backup The SQLite file plus the latest JSON export, because the export is the copy you can read when a schema change makes the database awkward. Take one before every upgrade. ## Restore Restore, run the round trip test, and check that the next ten occurrences for every recurring task match what the export recorded. A mismatch means the recurrence expansion changed, not that the backup was bad. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Recurring tasks whose materialised horizon has stopped advancing, views that reference a missing field, and any task sitting in a status with no category. All three are queries you can run from the interface. ## Incident checklist The incident here is work that stopped appearing, which is almost always a recurrence rule that hit its until date, consumed its count, or was lost in an import that only carried instances. 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 - [ ] No code is confirmed to compare a status by name - [ ] Views are confirmed free of stored SQL - [ ] The monthly 31st rule is confirmed to skip February without consuming the count - [ ] A due date is confirmed identical in two distant zones - [ ] A round trip is confirmed to reproduce the next occurrences ## Launch constraint Do not market omitted Height 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. Projects, tasks, views and recurrence rules. DATABASE_PATH=./data/tracker.db # Required. The whole access control model. Changing it should require thinking. BIND_ADDRESS=127.0.0.1 # Required. Local port for the interface. PORT=4321 # Required. Used for today, overdue and this week. Set it to something other than UTC while testing. USER_TIMEZONE=Pacific/Auckland # Required. How far recurring occurrences are generated in advance. MATERIALISE_AHEAD_DAYS=90 # Required. Completed tasks leave views after this, and stay in exports. ARCHIVE_AFTER_DAYS=30 # Required. Where the JSON and CSV exports are written. EXPORT_DIR=./exports
$ choose a build depth, inspect the files, then open the complete pack in your agent
Height: Teams paid because shared state, notifications, permissions, and migrations are harder than the visible board or list.
xenterprise permissions
xlarge integration catalog
xmobile capture and notifications
xreal-time multiplayer
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Vibecode Height
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Height replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Height cost?
Nothing anymore: Height shut down on September 24, 2025.
What do I lose by replacing Height?
Honestly: enterprise permissions; large integration catalog; mobile capture and notifications; real-time multiplayer. Since the product is gone, these gaps are now everyone's problem.
Is there an open-source alternative to Height?
Yes: YouTrack (Tasks, articles, no-code workflows, dashboards, and weekly AI credits for ten people) The prompt is for when you want it exactly your way.