Vibecode Zenkit
track this build4 phases, 12 steps, beginner friendly0%Zenkit's solo core is compact: build a single-user flexible work 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 Zenkit. 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 ===== # Zenkit · indie build A personal work manager whose hard problems are letting a list grow new fields without a schema migration every time, keeping every view a query rather than a second copy of the truth, and expanding recurring items correctly across February and a clock change. Estimated effort: **weekend**. 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 | One Node process serving server-rendered pages, no client framework | a work manager is lists and forms, and a build step is a tax paid every day | | Storage | SQLite in WAL mode, one file, custom field values in typed columns | the schema changes every time the user invents a field, so the schema must not change | | Recurrence | A documented subset of the iCalendar recurrence rule, stored as text and expanded on read | everyone who invents their own gets February wrong | | Time | Instants in UTC for anything with a time, plain dates for all-day items | a due date is not a moment and storing it as one moves it overnight | | Export | Whole workspace to JSON and any view to CSV, written in the first phase | the export you add at the end is the export nobody tests | ## 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 single process that serves the app. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **The sqlite3 command line tool** · free - Why: Needed to inspect the schema, confirm that adding a field ran no migration, and run a restore by hand. - Get it: Your package manager, or sqlite.org. - Verify: sqlite3 --version prints a version - [ ] **RFC 5545 section 3.3.10** · free - Why: The recurrence subset is copied from the specification rather than invented, and the rules about invalid dates and about count with an end date come straight from it. - Get it: Read the section and write down which parts of the rule you support. - Verify: A documented list of supported rule parts exists before any recurrence code - [ ] **Your field kinds, fixed in advance** · free - Why: Each kind needs a typed column, a sort, a filter and a form control, and adding a kind later touches all four, so the list is a decision rather than a discovery. - Get it: Write down the kinds you will support and the column each one writes into. - Verify: The list is in the README and matches the values table columns ## 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: - Multiplayer editing and permissions. - Email or push notifications. - A mobile application. - Automations and third-party integrations. - 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 Zenkit, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Zenkit 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 work manager whose boards, tables and calendars are all saved queries over one item table to replace Zenkit. 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: One Node process serving server-rendered pages, no client framework · a work manager is lists and forms, and a build step is a tax paid every day - Storage: SQLite in WAL mode, one file, custom field values in typed columns · the schema changes every time the user invents a field, so the schema must not change - Recurrence: A documented subset of the iCalendar recurrence rule, stored as text and expanded on read · everyone who invents their own gets February wrong - Time: Instants in UTC for anything with a time, plain dates for all-day items · a due date is not a moment and storing it as one moves it overnight - Export: Whole workspace to JSON and any view to CSV, written in the first phase · the export you add at the end is the export nobody tests ### Data model (create this before Phase 1) - lists(id, name, icon, position, created_at, archived_at) - fields(id, list_id, key, label, kind, options_json, position, required) · kind is text, number, date, datetime, select, multi_select, checkbox or person - items(id, list_id, title, status, position, created_at, updated_at, completed_at, deleted_at, parent_item_id) - field_values(item_id, field_id, text_value, num_value, date_value, datetime_utc, zone) · one typed column populated per row, never everything as text - views(id, list_id, name, shape, filter_json, sort_json, group_by_field_id) · shape is table, board, calendar or list - recurrences(id, item_id, rule_text, dtstart, zone, count_done, ends_at) · the rule, not a pile of generated rows - activity(id, item_id, at, kind, before_json, after_json) · append-only, and the source of undo - A view owns no items. It owns a filter, a sort and a grouping field, so the board and the table cannot disagree about what is true. ### Phase 1 · One item table that survives a new field Build: Items live in one table. Custom values live in a values table with a separate typed column per kind, and the field row says which column it uses. Adding, renaming and deleting a field is data, not schema. Deleting a field keeps its values until the list is emptied of them. Trap: The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. Done when: A number field sorts numerically and a date field sorts chronologically, both tested with values that sort wrongly as text. Adding a field to a list with a thousand items runs no schema change, verified by comparing the schema before and after. An empty value is distinguishable from a zero and from the text 'null'. Deleting a field hides it from views without deleting the item rows, and the values survive until the field is purged. Do not build yet: No views, recurrence, quick capture or export yet. ### Phase 2 · Views are saved queries, never second copies Build: A view stores a filter, a sort and a grouping field. The board groups by a select field and dragging a card writes that field. The calendar reads a date field. A view whose field was deleted degrades to a clear message rather than an empty page or a crash. Trap: Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. Done when: Dragging a card between board columns changes the grouping field and the table view shows the new value immediately, tested end to end. Every view derives from the item rows, asserted by there being no card or column table in the schema. A view referencing a deleted field renders a message naming the field instead of failing. Position within a group is stable across reloads and after an item is edited. Do not build yet: No recurrence, quick capture or export yet. ### Phase 3 · Recurring items that survive February Build: Store the rule text, the start and the zone, and expand occurrences on read within the window being displayed. Follow the iCalendar recurrence rule in RFC 5545 section 3.3.10 for the subset you support: it specifies that COUNT and UNTIL must not both appear in one rule, and that recurrence instances falling on invalid dates or times are ignored. Completing one occurrence must not complete the series, and editing the series must not silently rewrite past completions. Trap: Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. Done when: A rule for the 31st of each month produces no February occurrence and still produces the 31st of March, tested across a full year. A daily rule spanning a daylight saving change keeps its local time of day, tested with both transition dates. A rule containing both a count and an end date is rejected at input with the reason. Completing one occurrence leaves the series intact and records only that date as done. Do not build yet: No quick capture, undo or export yet. ### Phase 4 · Capture in three seconds, and undo Build: A single input that creates an item in the current list with a keyboard shortcut from anywhere in the app. Every write appends to the activity table with the before and after values, and undo replays the previous value. Deletion is a timestamp, and a purge is a separate deliberate action. Trap: Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. Done when: An item can be created from the keyboard without touching the mouse, tested end to end. Undo restores the previous value of the last write, including a field value and a completion, tested for both. A deleted item is recoverable until purge and appears in a trash view with its deletion time. Purge states how many items and values it will remove before doing it. Do not build yet: No export, backup or import yet. ### Phase 5 · Everything out, and back in again Build: Export the whole workspace as JSON including fields, values, views, recurrence rules and the activity log, and any view as CSV with the visible columns. Import the JSON back into an empty database and compare. Back up by copying the database with the SQLite backup interface rather than copying the file under write load. Trap: Copying a SQLite file with cp while the process is running can capture a database mid-transaction along with a stale write-ahead log, and the copy opens fine until the day you actually need it. Use the backup interface or a documented checkpoint, and restore one copy into a scratch directory as part of the routine rather than trusting it. Done when: Exporting a workspace and importing it into an empty database reproduces every item, field value, view and recurrence rule, compared field by field in a test. A CSV export of a view opens in a spreadsheet with dates readable and no column shifted by an unescaped comma, tested with a value containing a comma and a newline. A backup taken while writes are in flight restores to a consistent database, tested. The README names the file to copy and the command to restore it. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer editing and permissions, which turn every write into a conflict question and are most of what the subscription is for. - Notifications and reminders by email or push, which need a delivery path and a retry story bigger than the app. - A mobile application, since the phone case is capture and a web page handles that at a fraction of the effort. - Automations and integrations, which multiply the surface by the number of services and break when any of them changes. ### README must contain - That views are saved queries over one item table, so a board and a table can never disagree - Which subset of the iCalendar recurrence rule is supported, and that invalid dates are dropped rather than rounded - That all-day dates are stored as plain dates and timed items as instants, and why that matters at a clock change - How to export the workspace and how to import it back, with the round-trip test named - The exact backup command and a restore that has actually been run ===== AGENTS.md ===== # Agent instructions · Zenkit indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: One Node process serving server-rendered pages, no client framework, SQLite in WAL mode, one file, custom field values in typed columns, A documented subset of the iCalendar recurrence rule, stored as text and expanded on read, Instants in UTC for anything with a time, plain dates for all-day items, Whole workspace to JSON and any view to CSV, written in the first phase. 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 - The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. - Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. - Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. - Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. ===== BUILD_PLAN.md ===== # Build plan · Zenkit A personal work manager whose hard problems are letting a list grow new fields without a schema migration every time, keeping every view a query rather than a second copy of the truth, and expanding recurring items correctly across February and a clock change. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One item table that survives a new field A user adds a field to a list without a migration and without losing the ability to sort or filter on it. ### Steps 1. Give each value kind its own column One text column for everything is what makes sorting and filtering wrong later. Files: `src/db/schema.sql`, `src/fields.js` 2. Make field changes data rather than migrations The user invents fields faster than you ship releases. Files: `src/fields.js`, `tests/fields.test.js` 3. Test the sorts that text ordering gets wrong Nine against ten, and March against May. Files: `tests/sorting.test.js` ### Done when - [ ] Number and date fields sort by value, not as text - [ ] Adding a field runs no schema change - [ ] Empty, zero and the text 'null' stay distinct - [ ] Deleting a field keeps the underlying values ### Watch out - The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. ## Phase 2 · Views are saved queries, never second copies The same items shown as a table, a board and a calendar always agree. ### Steps 1. Store the filter, not the results A view that caches rows is a second truth that drifts by Friday. Files: `src/views.js` 2. Make a drag write one field The board is a grouping, so moving a card is an update. Files: `src/views.js`, `src/public/board.js` 3. Degrade a view whose field is gone Field deletion is common and should not produce a blank page. Files: `src/views.js`, `tests/views.test.js` ### Done when - [ ] A board drag updates the grouping field - [ ] No cards or columns table exists in the schema - [ ] A view with a deleted field explains itself - [ ] Ordering within a group survives a reload ### Watch out - Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. ## Phase 3 · Recurring items that survive February A repeating task generates the right dates across short months, clock changes and a completed occurrence. ### Steps 1. Expand from the rule, never from the last date Adding a month to a date is how the 31st becomes fiction. Files: `src/recurrence.js`, `tests/recurrence.test.js` 2. Drop invalid dates as the specification says Section 3.3.10 is explicit that they are ignored, not rounded. Files: `src/recurrence.js` 3. Separate one occurrence from the series Ticking Tuesday off should not end the habit. Files: `src/items.js`, `tests/occurrences.test.js` ### Done when - [ ] The 31st rule skips February and keeps March - [ ] A daily rule holds its local time across both clock changes - [ ] Count and end date together is rejected with a reason - [ ] Completing one occurrence leaves the series running ### Watch out - Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. ## Phase 4 · Capture in three seconds, and undo Adding a task is faster than opening a form, and every destructive action is reversible. ### Steps 1. Append every write to the activity table Before and after values, because undo has to know both. Files: `src/activity.js` 2. Make delete a timestamp A single-user app has no colleague with a copy. Files: `src/items.js`, `tests/trash.test.js` 3. Give capture a global shortcut Capture that needs navigation does not get used. Files: `src/public/capture.js` ### Done when - [ ] An item is created entirely from the keyboard - [ ] Undo restores a field value and a completion - [ ] Deleted items are recoverable from a trash view - [ ] Purge states the count before removing anything ### Watch out - Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. ## Not in this build - Multiplayer editing and permissions. - Email or push notifications. - A mobile application. - Automations and third-party integrations. ## After v1, if you want it - A read-only shared link for one view, which needs a token and no permission model - Subtasks rolled up into a parent item, once the filter language can express it - A weekly digest generated locally and saved as a file rather than sent ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The whole workspace. This is the file to back up. DATABASE_PATH=./data/work.db # Required. Where the backup interface writes copies, and where a restore is rehearsed from. BACKUP_DIR=./data/backups # Required. Used to expand recurrence rules and to render all-day dates. DEFAULT_TIMEZONE=Europe/London # Required. Bound to localhost. There is no authentication because there is one user. PORT=4180 # Required. How long a deleted item stays recoverable before purge offers to remove it. TRASH_RETENTION_DAYS=30
You are building a lean indie version of Zenkit. 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 ===== # Zenkit · indie build A personal work manager whose hard problems are letting a list grow new fields without a schema migration every time, keeping every view a query rather than a second copy of the truth, and expanding recurring items correctly across February and a clock change. Estimated effort: **weekend**. 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 | One Node process serving server-rendered pages, no client framework | a work manager is lists and forms, and a build step is a tax paid every day | | Storage | SQLite in WAL mode, one file, custom field values in typed columns | the schema changes every time the user invents a field, so the schema must not change | | Recurrence | A documented subset of the iCalendar recurrence rule, stored as text and expanded on read | everyone who invents their own gets February wrong | | Time | Instants in UTC for anything with a time, plain dates for all-day items | a due date is not a moment and storing it as one moves it overnight | | Export | Whole workspace to JSON and any view to CSV, written in the first phase | the export you add at the end is the export nobody tests | ## 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 single process that serves the app. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **The sqlite3 command line tool** · free - Why: Needed to inspect the schema, confirm that adding a field ran no migration, and run a restore by hand. - Get it: Your package manager, or sqlite.org. - Verify: sqlite3 --version prints a version - [ ] **RFC 5545 section 3.3.10** · free - Why: The recurrence subset is copied from the specification rather than invented, and the rules about invalid dates and about count with an end date come straight from it. - Get it: Read the section and write down which parts of the rule you support. - Verify: A documented list of supported rule parts exists before any recurrence code - [ ] **Your field kinds, fixed in advance** · free - Why: Each kind needs a typed column, a sort, a filter and a form control, and adding a kind later touches all four, so the list is a decision rather than a discovery. - Get it: Write down the kinds you will support and the column each one writes into. - Verify: The list is in the README and matches the values table columns ## 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: - Multiplayer editing and permissions. - Email or push notifications. - A mobile application. - Automations and third-party integrations. - 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 Zenkit, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Zenkit 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 work manager whose boards, tables and calendars are all saved queries over one item table to replace Zenkit. 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: One Node process serving server-rendered pages, no client framework · a work manager is lists and forms, and a build step is a tax paid every day - Storage: SQLite in WAL mode, one file, custom field values in typed columns · the schema changes every time the user invents a field, so the schema must not change - Recurrence: A documented subset of the iCalendar recurrence rule, stored as text and expanded on read · everyone who invents their own gets February wrong - Time: Instants in UTC for anything with a time, plain dates for all-day items · a due date is not a moment and storing it as one moves it overnight - Export: Whole workspace to JSON and any view to CSV, written in the first phase · the export you add at the end is the export nobody tests ### Data model (create this before Phase 1) - lists(id, name, icon, position, created_at, archived_at) - fields(id, list_id, key, label, kind, options_json, position, required) · kind is text, number, date, datetime, select, multi_select, checkbox or person - items(id, list_id, title, status, position, created_at, updated_at, completed_at, deleted_at, parent_item_id) - field_values(item_id, field_id, text_value, num_value, date_value, datetime_utc, zone) · one typed column populated per row, never everything as text - views(id, list_id, name, shape, filter_json, sort_json, group_by_field_id) · shape is table, board, calendar or list - recurrences(id, item_id, rule_text, dtstart, zone, count_done, ends_at) · the rule, not a pile of generated rows - activity(id, item_id, at, kind, before_json, after_json) · append-only, and the source of undo - A view owns no items. It owns a filter, a sort and a grouping field, so the board and the table cannot disagree about what is true. ### Phase 1 · One item table that survives a new field Build: Items live in one table. Custom values live in a values table with a separate typed column per kind, and the field row says which column it uses. Adding, renaming and deleting a field is data, not schema. Deleting a field keeps its values until the list is emptied of them. Trap: The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. Done when: A number field sorts numerically and a date field sorts chronologically, both tested with values that sort wrongly as text. Adding a field to a list with a thousand items runs no schema change, verified by comparing the schema before and after. An empty value is distinguishable from a zero and from the text 'null'. Deleting a field hides it from views without deleting the item rows, and the values survive until the field is purged. Do not build yet: No views, recurrence, quick capture or export yet. ### Phase 2 · Views are saved queries, never second copies Build: A view stores a filter, a sort and a grouping field. The board groups by a select field and dragging a card writes that field. The calendar reads a date field. A view whose field was deleted degrades to a clear message rather than an empty page or a crash. Trap: Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. Done when: Dragging a card between board columns changes the grouping field and the table view shows the new value immediately, tested end to end. Every view derives from the item rows, asserted by there being no card or column table in the schema. A view referencing a deleted field renders a message naming the field instead of failing. Position within a group is stable across reloads and after an item is edited. Do not build yet: No recurrence, quick capture or export yet. ### Phase 3 · Recurring items that survive February Build: Store the rule text, the start and the zone, and expand occurrences on read within the window being displayed. Follow the iCalendar recurrence rule in RFC 5545 section 3.3.10 for the subset you support: it specifies that COUNT and UNTIL must not both appear in one rule, and that recurrence instances falling on invalid dates or times are ignored. Completing one occurrence must not complete the series, and editing the series must not silently rewrite past completions. Trap: Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. Done when: A rule for the 31st of each month produces no February occurrence and still produces the 31st of March, tested across a full year. A daily rule spanning a daylight saving change keeps its local time of day, tested with both transition dates. A rule containing both a count and an end date is rejected at input with the reason. Completing one occurrence leaves the series intact and records only that date as done. Do not build yet: No quick capture, undo or export yet. ### Phase 4 · Capture in three seconds, and undo Build: A single input that creates an item in the current list with a keyboard shortcut from anywhere in the app. Every write appends to the activity table with the before and after values, and undo replays the previous value. Deletion is a timestamp, and a purge is a separate deliberate action. Trap: Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. Done when: An item can be created from the keyboard without touching the mouse, tested end to end. Undo restores the previous value of the last write, including a field value and a completion, tested for both. A deleted item is recoverable until purge and appears in a trash view with its deletion time. Purge states how many items and values it will remove before doing it. Do not build yet: No export, backup or import yet. ### Phase 5 · Everything out, and back in again Build: Export the whole workspace as JSON including fields, values, views, recurrence rules and the activity log, and any view as CSV with the visible columns. Import the JSON back into an empty database and compare. Back up by copying the database with the SQLite backup interface rather than copying the file under write load. Trap: Copying a SQLite file with cp while the process is running can capture a database mid-transaction along with a stale write-ahead log, and the copy opens fine until the day you actually need it. Use the backup interface or a documented checkpoint, and restore one copy into a scratch directory as part of the routine rather than trusting it. Done when: Exporting a workspace and importing it into an empty database reproduces every item, field value, view and recurrence rule, compared field by field in a test. A CSV export of a view opens in a spreadsheet with dates readable and no column shifted by an unescaped comma, tested with a value containing a comma and a newline. A backup taken while writes are in flight restores to a consistent database, tested. The README names the file to copy and the command to restore it. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer editing and permissions, which turn every write into a conflict question and are most of what the subscription is for. - Notifications and reminders by email or push, which need a delivery path and a retry story bigger than the app. - A mobile application, since the phone case is capture and a web page handles that at a fraction of the effort. - Automations and integrations, which multiply the surface by the number of services and break when any of them changes. ### README must contain - That views are saved queries over one item table, so a board and a table can never disagree - Which subset of the iCalendar recurrence rule is supported, and that invalid dates are dropped rather than rounded - That all-day dates are stored as plain dates and timed items as instants, and why that matters at a clock change - How to export the workspace and how to import it back, with the round-trip test named - The exact backup command and a restore that has actually been run ===== AGENTS.md ===== # Agent instructions · Zenkit indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: One Node process serving server-rendered pages, no client framework, SQLite in WAL mode, one file, custom field values in typed columns, A documented subset of the iCalendar recurrence rule, stored as text and expanded on read, Instants in UTC for anything with a time, plain dates for all-day items, Whole workspace to JSON and any view to CSV, written in the first phase. 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 - The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. - Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. - Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. - Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. ===== BUILD_PLAN.md ===== # Build plan · Zenkit A personal work manager whose hard problems are letting a list grow new fields without a schema migration every time, keeping every view a query rather than a second copy of the truth, and expanding recurring items correctly across February and a clock change. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One item table that survives a new field A user adds a field to a list without a migration and without losing the ability to sort or filter on it. ### Steps 1. Give each value kind its own column One text column for everything is what makes sorting and filtering wrong later. Files: `src/db/schema.sql`, `src/fields.js` 2. Make field changes data rather than migrations The user invents fields faster than you ship releases. Files: `src/fields.js`, `tests/fields.test.js` 3. Test the sorts that text ordering gets wrong Nine against ten, and March against May. Files: `tests/sorting.test.js` ### Done when - [ ] Number and date fields sort by value, not as text - [ ] Adding a field runs no schema change - [ ] Empty, zero and the text 'null' stay distinct - [ ] Deleting a field keeps the underlying values ### Watch out - The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. ## Phase 2 · Views are saved queries, never second copies The same items shown as a table, a board and a calendar always agree. ### Steps 1. Store the filter, not the results A view that caches rows is a second truth that drifts by Friday. Files: `src/views.js` 2. Make a drag write one field The board is a grouping, so moving a card is an update. Files: `src/views.js`, `src/public/board.js` 3. Degrade a view whose field is gone Field deletion is common and should not produce a blank page. Files: `src/views.js`, `tests/views.test.js` ### Done when - [ ] A board drag updates the grouping field - [ ] No cards or columns table exists in the schema - [ ] A view with a deleted field explains itself - [ ] Ordering within a group survives a reload ### Watch out - Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. ## Phase 3 · Recurring items that survive February A repeating task generates the right dates across short months, clock changes and a completed occurrence. ### Steps 1. Expand from the rule, never from the last date Adding a month to a date is how the 31st becomes fiction. Files: `src/recurrence.js`, `tests/recurrence.test.js` 2. Drop invalid dates as the specification says Section 3.3.10 is explicit that they are ignored, not rounded. Files: `src/recurrence.js` 3. Separate one occurrence from the series Ticking Tuesday off should not end the habit. Files: `src/items.js`, `tests/occurrences.test.js` ### Done when - [ ] The 31st rule skips February and keeps March - [ ] A daily rule holds its local time across both clock changes - [ ] Count and end date together is rejected with a reason - [ ] Completing one occurrence leaves the series running ### Watch out - Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. ## Phase 4 · Capture in three seconds, and undo Adding a task is faster than opening a form, and every destructive action is reversible. ### Steps 1. Append every write to the activity table Before and after values, because undo has to know both. Files: `src/activity.js` 2. Make delete a timestamp A single-user app has no colleague with a copy. Files: `src/items.js`, `tests/trash.test.js` 3. Give capture a global shortcut Capture that needs navigation does not get used. Files: `src/public/capture.js` ### Done when - [ ] An item is created entirely from the keyboard - [ ] Undo restores a field value and a completion - [ ] Deleted items are recoverable from a trash view - [ ] Purge states the count before removing anything ### Watch out - Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. ## Not in this build - Multiplayer editing and permissions. - Email or push notifications. - A mobile application. - Automations and third-party integrations. ## After v1, if you want it - A read-only shared link for one view, which needs a token and no permission model - Subtasks rolled up into a parent item, once the filter language can express it - A weekly digest generated locally and saved as a file rather than sent ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The whole workspace. This is the file to back up. DATABASE_PATH=./data/work.db # Required. Where the backup interface writes copies, and where a restore is rehearsed from. BACKUP_DIR=./data/backups # Required. Used to expand recurrence rules and to render all-day dates. DEFAULT_TIMEZONE=Europe/London # Required. Bound to localhost. There is no authentication because there is one user. PORT=4180 # Required. How long a deleted item stays recoverable before purge offers to remove it. TRASH_RETENTION_DAYS=30
You are building a production product version of Zenkit. 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 ===== # Zenkit · product brief ## Problem Zenkit's solo core is compact: build a single-user flexible work 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 work manager where a new field costs nothing, every view is the same truth shown differently, a monthly task behaves in February, and the whole workspace can be exported and imported back without loss. ## 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 - Multiplayer editing and permissions. - Email or push notifications. - A mobile application. - Automations and third-party integrations. - enterprise permissions - large integration catalog - mobile capture and notifications - real-time multiplayer ## Success criteria - Number and date fields are confirmed to sort by value - A board drag is confirmed to write one field on the item - The monthly 31st rule is confirmed to skip February - Undo is confirmed to restore a field value and a completion - Export then import is confirmed to reproduce the workspace ===== BRIEF.md ===== # Build brief · Zenkit 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 work manager whose boards, tables and calendars are all saved queries over one item table to replace Zenkit. 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: One Node process serving server-rendered pages, no client framework · a work manager is lists and forms, and a build step is a tax paid every day - Storage: SQLite in WAL mode, one file, custom field values in typed columns · the schema changes every time the user invents a field, so the schema must not change - Recurrence: A documented subset of the iCalendar recurrence rule, stored as text and expanded on read · everyone who invents their own gets February wrong - Time: Instants in UTC for anything with a time, plain dates for all-day items · a due date is not a moment and storing it as one moves it overnight - Export: Whole workspace to JSON and any view to CSV, written in the first phase · the export you add at the end is the export nobody tests ### Data model (create this before Phase 1) - lists(id, name, icon, position, created_at, archived_at) - fields(id, list_id, key, label, kind, options_json, position, required) · kind is text, number, date, datetime, select, multi_select, checkbox or person - items(id, list_id, title, status, position, created_at, updated_at, completed_at, deleted_at, parent_item_id) - field_values(item_id, field_id, text_value, num_value, date_value, datetime_utc, zone) · one typed column populated per row, never everything as text - views(id, list_id, name, shape, filter_json, sort_json, group_by_field_id) · shape is table, board, calendar or list - recurrences(id, item_id, rule_text, dtstart, zone, count_done, ends_at) · the rule, not a pile of generated rows - activity(id, item_id, at, kind, before_json, after_json) · append-only, and the source of undo - A view owns no items. It owns a filter, a sort and a grouping field, so the board and the table cannot disagree about what is true. ### Phase 1 · One item table that survives a new field Build: Items live in one table. Custom values live in a values table with a separate typed column per kind, and the field row says which column it uses. Adding, renaming and deleting a field is data, not schema. Deleting a field keeps its values until the list is emptied of them. Trap: The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. Done when: A number field sorts numerically and a date field sorts chronologically, both tested with values that sort wrongly as text. Adding a field to a list with a thousand items runs no schema change, verified by comparing the schema before and after. An empty value is distinguishable from a zero and from the text 'null'. Deleting a field hides it from views without deleting the item rows, and the values survive until the field is purged. Do not build yet: No views, recurrence, quick capture or export yet. ### Phase 2 · Views are saved queries, never second copies Build: A view stores a filter, a sort and a grouping field. The board groups by a select field and dragging a card writes that field. The calendar reads a date field. A view whose field was deleted degrades to a clear message rather than an empty page or a crash. Trap: Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. Done when: Dragging a card between board columns changes the grouping field and the table view shows the new value immediately, tested end to end. Every view derives from the item rows, asserted by there being no card or column table in the schema. A view referencing a deleted field renders a message naming the field instead of failing. Position within a group is stable across reloads and after an item is edited. Do not build yet: No recurrence, quick capture or export yet. ### Phase 3 · Recurring items that survive February Build: Store the rule text, the start and the zone, and expand occurrences on read within the window being displayed. Follow the iCalendar recurrence rule in RFC 5545 section 3.3.10 for the subset you support: it specifies that COUNT and UNTIL must not both appear in one rule, and that recurrence instances falling on invalid dates or times are ignored. Completing one occurrence must not complete the series, and editing the series must not silently rewrite past completions. Trap: Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. Done when: A rule for the 31st of each month produces no February occurrence and still produces the 31st of March, tested across a full year. A daily rule spanning a daylight saving change keeps its local time of day, tested with both transition dates. A rule containing both a count and an end date is rejected at input with the reason. Completing one occurrence leaves the series intact and records only that date as done. Do not build yet: No quick capture, undo or export yet. ### Phase 4 · Capture in three seconds, and undo Build: A single input that creates an item in the current list with a keyboard shortcut from anywhere in the app. Every write appends to the activity table with the before and after values, and undo replays the previous value. Deletion is a timestamp, and a purge is a separate deliberate action. Trap: Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. Done when: An item can be created from the keyboard without touching the mouse, tested end to end. Undo restores the previous value of the last write, including a field value and a completion, tested for both. A deleted item is recoverable until purge and appears in a trash view with its deletion time. Purge states how many items and values it will remove before doing it. Do not build yet: No export, backup or import yet. ### Phase 5 · Everything out, and back in again Build: Export the whole workspace as JSON including fields, values, views, recurrence rules and the activity log, and any view as CSV with the visible columns. Import the JSON back into an empty database and compare. Back up by copying the database with the SQLite backup interface rather than copying the file under write load. Trap: Copying a SQLite file with cp while the process is running can capture a database mid-transaction along with a stale write-ahead log, and the copy opens fine until the day you actually need it. Use the backup interface or a documented checkpoint, and restore one copy into a scratch directory as part of the routine rather than trusting it. Done when: Exporting a workspace and importing it into an empty database reproduces every item, field value, view and recurrence rule, compared field by field in a test. A CSV export of a view opens in a spreadsheet with dates readable and no column shifted by an unescaped comma, tested with a value containing a comma and a newline. A backup taken while writes are in flight restores to a consistent database, tested. The README names the file to copy and the command to restore it. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer editing and permissions, which turn every write into a conflict question and are most of what the subscription is for. - Notifications and reminders by email or push, which need a delivery path and a retry story bigger than the app. - A mobile application, since the phone case is capture and a web page handles that at a fraction of the effort. - Automations and integrations, which multiply the surface by the number of services and break when any of them changes. ### README must contain - That views are saved queries over one item table, so a board and a table can never disagree - Which subset of the iCalendar recurrence rule is supported, and that invalid dates are dropped rather than rounded - That all-day dates are stored as plain dates and timed items as instants, and why that matters at a clock change - How to export the workspace and how to import it back, with the round-trip test named - The exact backup command and a restore that has actually been run ===== ARCHITECTURE.md ===== # Architecture · Zenkit ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | One Node process serving server-rendered pages, no client framework | a work manager is lists and forms, and a build step is a tax paid every day | | Storage | SQLite in WAL mode, one file, custom field values in typed columns | the schema changes every time the user invents a field, so the schema must not change | | Recurrence | A documented subset of the iCalendar recurrence rule, stored as text and expanded on read | everyone who invents their own gets February wrong | | Time | Instants in UTC for anything with a time, plain dates for all-day items | a due date is not a moment and storing it as one moves it overnight | | Export | Whole workspace to JSON and any view to CSV, written in the first phase | the export you add at the end is the export nobody tests | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Fields | Typed values and field lifecycle. | Any store, provided values stay typed and a field change is not a migration. | | Views | Filters, sorts and grouping. | Any presentation, provided no view keeps its own copy of the rows. | | Recurrence | Rule text and expansion. | A maintained recurrence library, which is preferable to your own. | | Activity | The append-only log and undo. | Not swappable for overwriting in place, which removes undo. | | Export | JSON round trip and CSV. | Any format, provided the round trip is tested rather than described. | ## 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 · The whole workspace. This is the file to back up. - `BACKUP_DIR` · required · Where the backup interface writes copies, and where a restore is rehearsed from. - `DEFAULT_TIMEZONE` · required · Used to expand recurrence rules and to render all-day dates. - `PORT` · required · Bound to localhost. There is no authentication because there is one user. - `TRASH_RETENTION_DAYS` · required · How long a deleted item stays recoverable before purge offers to remove it. ## 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 · Zenkit product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: One Node process serving server-rendered pages, no client framework, SQLite in WAL mode, one file, custom field values in typed columns, A documented subset of the iCalendar recurrence rule, stored as text and expanded on read, Instants in UTC for anything with a time, plain dates for all-day items, Whole workspace to JSON and any view to CSV, written in the first phase. - 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 - The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. - Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. - Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. - Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. - Copying a SQLite file with cp while the process is running can capture a database mid-transaction along with a stale write-ahead log, and the copy opens fine until the day you actually need it. Use the backup interface or a documented checkpoint, and restore one copy into a scratch directory as part of the routine rather than trusting it. ===== MILESTONES.md ===== # Delivery milestones · Zenkit Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One item table that survives a new field A user adds a field to a list without a migration and without losing the ability to sort or filter on it. ### Steps 1. Give each value kind its own column One text column for everything is what makes sorting and filtering wrong later. Files: `src/db/schema.sql`, `src/fields.js` 2. Make field changes data rather than migrations The user invents fields faster than you ship releases. Files: `src/fields.js`, `tests/fields.test.js` 3. Test the sorts that text ordering gets wrong Nine against ten, and March against May. Files: `tests/sorting.test.js` ### Done when - [ ] Number and date fields sort by value, not as text - [ ] Adding a field runs no schema change - [ ] Empty, zero and the text 'null' stay distinct - [ ] Deleting a field keeps the underlying values ### Watch out - The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. ## M2 · Views are saved queries, never second copies The same items shown as a table, a board and a calendar always agree. ### Steps 1. Store the filter, not the results A view that caches rows is a second truth that drifts by Friday. Files: `src/views.js` 2. Make a drag write one field The board is a grouping, so moving a card is an update. Files: `src/views.js`, `src/public/board.js` 3. Degrade a view whose field is gone Field deletion is common and should not produce a blank page. Files: `src/views.js`, `tests/views.test.js` ### Done when - [ ] A board drag updates the grouping field - [ ] No cards or columns table exists in the schema - [ ] A view with a deleted field explains itself - [ ] Ordering within a group survives a reload ### Watch out - Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. ## M3 · Recurring items that survive February A repeating task generates the right dates across short months, clock changes and a completed occurrence. ### Steps 1. Expand from the rule, never from the last date Adding a month to a date is how the 31st becomes fiction. Files: `src/recurrence.js`, `tests/recurrence.test.js` 2. Drop invalid dates as the specification says Section 3.3.10 is explicit that they are ignored, not rounded. Files: `src/recurrence.js` 3. Separate one occurrence from the series Ticking Tuesday off should not end the habit. Files: `src/items.js`, `tests/occurrences.test.js` ### Done when - [ ] The 31st rule skips February and keeps March - [ ] A daily rule holds its local time across both clock changes - [ ] Count and end date together is rejected with a reason - [ ] Completing one occurrence leaves the series running ### Watch out - Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. ## M4 · Capture in three seconds, and undo Adding a task is faster than opening a form, and every destructive action is reversible. ### Steps 1. Append every write to the activity table Before and after values, because undo has to know both. Files: `src/activity.js` 2. Make delete a timestamp A single-user app has no colleague with a copy. Files: `src/items.js`, `tests/trash.test.js` 3. Give capture a global shortcut Capture that needs navigation does not get used. Files: `src/public/capture.js` ### Done when - [ ] An item is created entirely from the keyboard - [ ] Undo restores a field value and a completion - [ ] Deleted items are recoverable from a trash view - [ ] Purge states the count before removing anything ### Watch out - Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. ## M5 · Everything out, and back in again (production only) The workspace can leave this app intact and return without loss. ### Steps 1. Round-trip the export in a test An export nobody has imported is a guess about the future. Files: `src/export.js`, `tests/roundtrip.test.js` 2. Escape CSV against real values Commas, quotes and newlines live in every notes field. Files: `src/csv.js`, `tests/csv.test.js` 3. Back up through the backup interface Copying the file under load produces a copy that opens until it matters. Files: `src/backup.js`, `docs/restore.md` ### Done when - [ ] Export then import reproduces the workspace field by field - [ ] CSV survives commas, quotes and newlines - [ ] A backup under write load restores consistently - [ ] The README names the restore command ### Watch out - Copying a SQLite file with cp while the process is running can capture a database mid-transaction along with a stale write-ahead log, and the copy opens fine until the day you actually need it. Use the backup interface or a documented checkpoint, and restore one copy into a scratch directory as part of the routine rather than trusting it. ===== OPERATIONS.md ===== # Operations · Zenkit ## Backup Copy the database through the SQLite backup interface on a schedule, keep several generations, and hold one copy off the machine. A copy taken with cp under write load is the failure mode this avoids. ## Restore Restore into a scratch directory, open it, confirm the item count and a recurring item's next occurrence, then swap. Rehearse it once before you need it. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a single-user app the checks belong in the suite: the round-trip export, both clock change dates, the 31st rule, and a restore of a backup taken under load. ## Incident checklist The two incidents are a lost item and a wrong date. The activity log answers the first and names the write that did it; the recurrence tests answer the second, which is why the transition dates are in the suite rather than in a note. 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 - [ ] Number and date fields are confirmed to sort by value - [ ] A board drag is confirmed to write one field on the item - [ ] The monthly 31st rule is confirmed to skip February - [ ] Undo is confirmed to restore a field value and a completion - [ ] Export then import is confirmed to reproduce the workspace ## Launch constraint Do not market omitted Zenkit 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. The whole workspace. This is the file to back up. DATABASE_PATH=./data/work.db # Required. Where the backup interface writes copies, and where a restore is rehearsed from. BACKUP_DIR=./data/backups # Required. Used to expand recurrence rules and to render all-day dates. DEFAULT_TIMEZONE=Europe/London # Required. Bound to localhost. There is no authentication because there is one user. PORT=4180 # Required. How long a deleted item stays recoverable before purge offers to remove it. TRASH_RETENTION_DAYS=30
# Zenkit · indie build A personal work manager whose hard problems are letting a list grow new fields without a schema migration every time, keeping every view a query rather than a second copy of the truth, and expanding recurring items correctly across February and a clock change. Estimated effort: **weekend**. 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 | One Node process serving server-rendered pages, no client framework | a work manager is lists and forms, and a build step is a tax paid every day | | Storage | SQLite in WAL mode, one file, custom field values in typed columns | the schema changes every time the user invents a field, so the schema must not change | | Recurrence | A documented subset of the iCalendar recurrence rule, stored as text and expanded on read | everyone who invents their own gets February wrong | | Time | Instants in UTC for anything with a time, plain dates for all-day items | a due date is not a moment and storing it as one moves it overnight | | Export | Whole workspace to JSON and any view to CSV, written in the first phase | the export you add at the end is the export nobody tests | ## 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 single process that serves the app. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **The sqlite3 command line tool** · free - Why: Needed to inspect the schema, confirm that adding a field ran no migration, and run a restore by hand. - Get it: Your package manager, or sqlite.org. - Verify: sqlite3 --version prints a version - [ ] **RFC 5545 section 3.3.10** · free - Why: The recurrence subset is copied from the specification rather than invented, and the rules about invalid dates and about count with an end date come straight from it. - Get it: Read the section and write down which parts of the rule you support. - Verify: A documented list of supported rule parts exists before any recurrence code - [ ] **Your field kinds, fixed in advance** · free - Why: Each kind needs a typed column, a sort, a filter and a form control, and adding a kind later touches all four, so the list is a decision rather than a discovery. - Get it: Write down the kinds you will support and the column each one writes into. - Verify: The list is in the README and matches the values table columns ## 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: - Multiplayer editing and permissions. - Email or push notifications. - A mobile application. - Automations and third-party integrations. - 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 Zenkit, and the README should say so rather than pretend.
# Build brief · Zenkit 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 work manager whose boards, tables and calendars are all saved queries over one item table to replace Zenkit. 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: One Node process serving server-rendered pages, no client framework · a work manager is lists and forms, and a build step is a tax paid every day - Storage: SQLite in WAL mode, one file, custom field values in typed columns · the schema changes every time the user invents a field, so the schema must not change - Recurrence: A documented subset of the iCalendar recurrence rule, stored as text and expanded on read · everyone who invents their own gets February wrong - Time: Instants in UTC for anything with a time, plain dates for all-day items · a due date is not a moment and storing it as one moves it overnight - Export: Whole workspace to JSON and any view to CSV, written in the first phase · the export you add at the end is the export nobody tests ### Data model (create this before Phase 1) - lists(id, name, icon, position, created_at, archived_at) - fields(id, list_id, key, label, kind, options_json, position, required) · kind is text, number, date, datetime, select, multi_select, checkbox or person - items(id, list_id, title, status, position, created_at, updated_at, completed_at, deleted_at, parent_item_id) - field_values(item_id, field_id, text_value, num_value, date_value, datetime_utc, zone) · one typed column populated per row, never everything as text - views(id, list_id, name, shape, filter_json, sort_json, group_by_field_id) · shape is table, board, calendar or list - recurrences(id, item_id, rule_text, dtstart, zone, count_done, ends_at) · the rule, not a pile of generated rows - activity(id, item_id, at, kind, before_json, after_json) · append-only, and the source of undo - A view owns no items. It owns a filter, a sort and a grouping field, so the board and the table cannot disagree about what is true. ### Phase 1 · One item table that survives a new field Build: Items live in one table. Custom values live in a values table with a separate typed column per kind, and the field row says which column it uses. Adding, renaming and deleting a field is data, not schema. Deleting a field keeps its values until the list is emptied of them. Trap: The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. Done when: A number field sorts numerically and a date field sorts chronologically, both tested with values that sort wrongly as text. Adding a field to a list with a thousand items runs no schema change, verified by comparing the schema before and after. An empty value is distinguishable from a zero and from the text 'null'. Deleting a field hides it from views without deleting the item rows, and the values survive until the field is purged. Do not build yet: No views, recurrence, quick capture or export yet. ### Phase 2 · Views are saved queries, never second copies Build: A view stores a filter, a sort and a grouping field. The board groups by a select field and dragging a card writes that field. The calendar reads a date field. A view whose field was deleted degrades to a clear message rather than an empty page or a crash. Trap: Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. Done when: Dragging a card between board columns changes the grouping field and the table view shows the new value immediately, tested end to end. Every view derives from the item rows, asserted by there being no card or column table in the schema. A view referencing a deleted field renders a message naming the field instead of failing. Position within a group is stable across reloads and after an item is edited. Do not build yet: No recurrence, quick capture or export yet. ### Phase 3 · Recurring items that survive February Build: Store the rule text, the start and the zone, and expand occurrences on read within the window being displayed. Follow the iCalendar recurrence rule in RFC 5545 section 3.3.10 for the subset you support: it specifies that COUNT and UNTIL must not both appear in one rule, and that recurrence instances falling on invalid dates or times are ignored. Completing one occurrence must not complete the series, and editing the series must not silently rewrite past completions. Trap: Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. Done when: A rule for the 31st of each month produces no February occurrence and still produces the 31st of March, tested across a full year. A daily rule spanning a daylight saving change keeps its local time of day, tested with both transition dates. A rule containing both a count and an end date is rejected at input with the reason. Completing one occurrence leaves the series intact and records only that date as done. Do not build yet: No quick capture, undo or export yet. ### Phase 4 · Capture in three seconds, and undo Build: A single input that creates an item in the current list with a keyboard shortcut from anywhere in the app. Every write appends to the activity table with the before and after values, and undo replays the previous value. Deletion is a timestamp, and a purge is a separate deliberate action. Trap: Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. Done when: An item can be created from the keyboard without touching the mouse, tested end to end. Undo restores the previous value of the last write, including a field value and a completion, tested for both. A deleted item is recoverable until purge and appears in a trash view with its deletion time. Purge states how many items and values it will remove before doing it. Do not build yet: No export, backup or import yet. ### Phase 5 · Everything out, and back in again Build: Export the whole workspace as JSON including fields, values, views, recurrence rules and the activity log, and any view as CSV with the visible columns. Import the JSON back into an empty database and compare. Back up by copying the database with the SQLite backup interface rather than copying the file under write load. Trap: Copying a SQLite file with cp while the process is running can capture a database mid-transaction along with a stale write-ahead log, and the copy opens fine until the day you actually need it. Use the backup interface or a documented checkpoint, and restore one copy into a scratch directory as part of the routine rather than trusting it. Done when: Exporting a workspace and importing it into an empty database reproduces every item, field value, view and recurrence rule, compared field by field in a test. A CSV export of a view opens in a spreadsheet with dates readable and no column shifted by an unescaped comma, tested with a value containing a comma and a newline. A backup taken while writes are in flight restores to a consistent database, tested. The README names the file to copy and the command to restore it. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer editing and permissions, which turn every write into a conflict question and are most of what the subscription is for. - Notifications and reminders by email or push, which need a delivery path and a retry story bigger than the app. - A mobile application, since the phone case is capture and a web page handles that at a fraction of the effort. - Automations and integrations, which multiply the surface by the number of services and break when any of them changes. ### README must contain - That views are saved queries over one item table, so a board and a table can never disagree - Which subset of the iCalendar recurrence rule is supported, and that invalid dates are dropped rather than rounded - That all-day dates are stored as plain dates and timed items as instants, and why that matters at a clock change - How to export the workspace and how to import it back, with the round-trip test named - The exact backup command and a restore that has actually been run
# Agent instructions · Zenkit indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: One Node process serving server-rendered pages, no client framework, SQLite in WAL mode, one file, custom field values in typed columns, A documented subset of the iCalendar recurrence rule, stored as text and expanded on read, Instants in UTC for anything with a time, plain dates for all-day items, Whole workspace to JSON and any view to CSV, written in the first phase. 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 - The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. - Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. - Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. - Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go.
# Build plan · Zenkit A personal work manager whose hard problems are letting a list grow new fields without a schema migration every time, keeping every view a query rather than a second copy of the truth, and expanding recurring items correctly across February and a clock change. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One item table that survives a new field A user adds a field to a list without a migration and without losing the ability to sort or filter on it. ### Steps 1. Give each value kind its own column One text column for everything is what makes sorting and filtering wrong later. Files: `src/db/schema.sql`, `src/fields.js` 2. Make field changes data rather than migrations The user invents fields faster than you ship releases. Files: `src/fields.js`, `tests/fields.test.js` 3. Test the sorts that text ordering gets wrong Nine against ten, and March against May. Files: `tests/sorting.test.js` ### Done when - [ ] Number and date fields sort by value, not as text - [ ] Adding a field runs no schema change - [ ] Empty, zero and the text 'null' stay distinct - [ ] Deleting a field keeps the underlying values ### Watch out - The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. ## Phase 2 · Views are saved queries, never second copies The same items shown as a table, a board and a calendar always agree. ### Steps 1. Store the filter, not the results A view that caches rows is a second truth that drifts by Friday. Files: `src/views.js` 2. Make a drag write one field The board is a grouping, so moving a card is an update. Files: `src/views.js`, `src/public/board.js` 3. Degrade a view whose field is gone Field deletion is common and should not produce a blank page. Files: `src/views.js`, `tests/views.test.js` ### Done when - [ ] A board drag updates the grouping field - [ ] No cards or columns table exists in the schema - [ ] A view with a deleted field explains itself - [ ] Ordering within a group survives a reload ### Watch out - Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. ## Phase 3 · Recurring items that survive February A repeating task generates the right dates across short months, clock changes and a completed occurrence. ### Steps 1. Expand from the rule, never from the last date Adding a month to a date is how the 31st becomes fiction. Files: `src/recurrence.js`, `tests/recurrence.test.js` 2. Drop invalid dates as the specification says Section 3.3.10 is explicit that they are ignored, not rounded. Files: `src/recurrence.js` 3. Separate one occurrence from the series Ticking Tuesday off should not end the habit. Files: `src/items.js`, `tests/occurrences.test.js` ### Done when - [ ] The 31st rule skips February and keeps March - [ ] A daily rule holds its local time across both clock changes - [ ] Count and end date together is rejected with a reason - [ ] Completing one occurrence leaves the series running ### Watch out - Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. ## Phase 4 · Capture in three seconds, and undo Adding a task is faster than opening a form, and every destructive action is reversible. ### Steps 1. Append every write to the activity table Before and after values, because undo has to know both. Files: `src/activity.js` 2. Make delete a timestamp A single-user app has no colleague with a copy. Files: `src/items.js`, `tests/trash.test.js` 3. Give capture a global shortcut Capture that needs navigation does not get used. Files: `src/public/capture.js` ### Done when - [ ] An item is created entirely from the keyboard - [ ] Undo restores a field value and a completion - [ ] Deleted items are recoverable from a trash view - [ ] Purge states the count before removing anything ### Watch out - Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. ## Not in this build - Multiplayer editing and permissions. - Email or push notifications. - A mobile application. - Automations and third-party integrations. ## After v1, if you want it - A read-only shared link for one view, which needs a token and no permission model - Subtasks rolled up into a parent item, once the filter language can express it - A weekly digest generated locally and saved as a file rather than sent
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The whole workspace. This is the file to back up. DATABASE_PATH=./data/work.db # Required. Where the backup interface writes copies, and where a restore is rehearsed from. BACKUP_DIR=./data/backups # Required. Used to expand recurrence rules and to render all-day dates. DEFAULT_TIMEZONE=Europe/London # Required. Bound to localhost. There is no authentication because there is one user. PORT=4180 # Required. How long a deleted item stays recoverable before purge offers to remove it. TRASH_RETENTION_DAYS=30
# Zenkit · product brief ## Problem Zenkit's solo core is compact: build a single-user flexible work 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 work manager where a new field costs nothing, every view is the same truth shown differently, a monthly task behaves in February, and the whole workspace can be exported and imported back without loss. ## 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 - Multiplayer editing and permissions. - Email or push notifications. - A mobile application. - Automations and third-party integrations. - enterprise permissions - large integration catalog - mobile capture and notifications - real-time multiplayer ## Success criteria - Number and date fields are confirmed to sort by value - A board drag is confirmed to write one field on the item - The monthly 31st rule is confirmed to skip February - Undo is confirmed to restore a field value and a completion - Export then import is confirmed to reproduce the workspace
# Build brief · Zenkit 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 work manager whose boards, tables and calendars are all saved queries over one item table to replace Zenkit. 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: One Node process serving server-rendered pages, no client framework · a work manager is lists and forms, and a build step is a tax paid every day - Storage: SQLite in WAL mode, one file, custom field values in typed columns · the schema changes every time the user invents a field, so the schema must not change - Recurrence: A documented subset of the iCalendar recurrence rule, stored as text and expanded on read · everyone who invents their own gets February wrong - Time: Instants in UTC for anything with a time, plain dates for all-day items · a due date is not a moment and storing it as one moves it overnight - Export: Whole workspace to JSON and any view to CSV, written in the first phase · the export you add at the end is the export nobody tests ### Data model (create this before Phase 1) - lists(id, name, icon, position, created_at, archived_at) - fields(id, list_id, key, label, kind, options_json, position, required) · kind is text, number, date, datetime, select, multi_select, checkbox or person - items(id, list_id, title, status, position, created_at, updated_at, completed_at, deleted_at, parent_item_id) - field_values(item_id, field_id, text_value, num_value, date_value, datetime_utc, zone) · one typed column populated per row, never everything as text - views(id, list_id, name, shape, filter_json, sort_json, group_by_field_id) · shape is table, board, calendar or list - recurrences(id, item_id, rule_text, dtstart, zone, count_done, ends_at) · the rule, not a pile of generated rows - activity(id, item_id, at, kind, before_json, after_json) · append-only, and the source of undo - A view owns no items. It owns a filter, a sort and a grouping field, so the board and the table cannot disagree about what is true. ### Phase 1 · One item table that survives a new field Build: Items live in one table. Custom values live in a values table with a separate typed column per kind, and the field row says which column it uses. Adding, renaming and deleting a field is data, not schema. Deleting a field keeps its values until the list is emptied of them. Trap: The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. Done when: A number field sorts numerically and a date field sorts chronologically, both tested with values that sort wrongly as text. Adding a field to a list with a thousand items runs no schema change, verified by comparing the schema before and after. An empty value is distinguishable from a zero and from the text 'null'. Deleting a field hides it from views without deleting the item rows, and the values survive until the field is purged. Do not build yet: No views, recurrence, quick capture or export yet. ### Phase 2 · Views are saved queries, never second copies Build: A view stores a filter, a sort and a grouping field. The board groups by a select field and dragging a card writes that field. The calendar reads a date field. A view whose field was deleted degrades to a clear message rather than an empty page or a crash. Trap: Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. Done when: Dragging a card between board columns changes the grouping field and the table view shows the new value immediately, tested end to end. Every view derives from the item rows, asserted by there being no card or column table in the schema. A view referencing a deleted field renders a message naming the field instead of failing. Position within a group is stable across reloads and after an item is edited. Do not build yet: No recurrence, quick capture or export yet. ### Phase 3 · Recurring items that survive February Build: Store the rule text, the start and the zone, and expand occurrences on read within the window being displayed. Follow the iCalendar recurrence rule in RFC 5545 section 3.3.10 for the subset you support: it specifies that COUNT and UNTIL must not both appear in one rule, and that recurrence instances falling on invalid dates or times are ignored. Completing one occurrence must not complete the series, and editing the series must not silently rewrite past completions. Trap: Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. Done when: A rule for the 31st of each month produces no February occurrence and still produces the 31st of March, tested across a full year. A daily rule spanning a daylight saving change keeps its local time of day, tested with both transition dates. A rule containing both a count and an end date is rejected at input with the reason. Completing one occurrence leaves the series intact and records only that date as done. Do not build yet: No quick capture, undo or export yet. ### Phase 4 · Capture in three seconds, and undo Build: A single input that creates an item in the current list with a keyboard shortcut from anywhere in the app. Every write appends to the activity table with the before and after values, and undo replays the previous value. Deletion is a timestamp, and a purge is a separate deliberate action. Trap: Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. Done when: An item can be created from the keyboard without touching the mouse, tested end to end. Undo restores the previous value of the last write, including a field value and a completion, tested for both. A deleted item is recoverable until purge and appears in a trash view with its deletion time. Purge states how many items and values it will remove before doing it. Do not build yet: No export, backup or import yet. ### Phase 5 · Everything out, and back in again Build: Export the whole workspace as JSON including fields, values, views, recurrence rules and the activity log, and any view as CSV with the visible columns. Import the JSON back into an empty database and compare. Back up by copying the database with the SQLite backup interface rather than copying the file under write load. Trap: Copying a SQLite file with cp while the process is running can capture a database mid-transaction along with a stale write-ahead log, and the copy opens fine until the day you actually need it. Use the backup interface or a documented checkpoint, and restore one copy into a scratch directory as part of the routine rather than trusting it. Done when: Exporting a workspace and importing it into an empty database reproduces every item, field value, view and recurrence rule, compared field by field in a test. A CSV export of a view opens in a spreadsheet with dates readable and no column shifted by an unescaped comma, tested with a value containing a comma and a newline. A backup taken while writes are in flight restores to a consistent database, tested. The README names the file to copy and the command to restore it. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer editing and permissions, which turn every write into a conflict question and are most of what the subscription is for. - Notifications and reminders by email or push, which need a delivery path and a retry story bigger than the app. - A mobile application, since the phone case is capture and a web page handles that at a fraction of the effort. - Automations and integrations, which multiply the surface by the number of services and break when any of them changes. ### README must contain - That views are saved queries over one item table, so a board and a table can never disagree - Which subset of the iCalendar recurrence rule is supported, and that invalid dates are dropped rather than rounded - That all-day dates are stored as plain dates and timed items as instants, and why that matters at a clock change - How to export the workspace and how to import it back, with the round-trip test named - The exact backup command and a restore that has actually been run
# Architecture · Zenkit ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | One Node process serving server-rendered pages, no client framework | a work manager is lists and forms, and a build step is a tax paid every day | | Storage | SQLite in WAL mode, one file, custom field values in typed columns | the schema changes every time the user invents a field, so the schema must not change | | Recurrence | A documented subset of the iCalendar recurrence rule, stored as text and expanded on read | everyone who invents their own gets February wrong | | Time | Instants in UTC for anything with a time, plain dates for all-day items | a due date is not a moment and storing it as one moves it overnight | | Export | Whole workspace to JSON and any view to CSV, written in the first phase | the export you add at the end is the export nobody tests | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Fields | Typed values and field lifecycle. | Any store, provided values stay typed and a field change is not a migration. | | Views | Filters, sorts and grouping. | Any presentation, provided no view keeps its own copy of the rows. | | Recurrence | Rule text and expansion. | A maintained recurrence library, which is preferable to your own. | | Activity | The append-only log and undo. | Not swappable for overwriting in place, which removes undo. | | Export | JSON round trip and CSV. | Any format, provided the round trip is tested rather than described. | ## 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 · The whole workspace. This is the file to back up. - `BACKUP_DIR` · required · Where the backup interface writes copies, and where a restore is rehearsed from. - `DEFAULT_TIMEZONE` · required · Used to expand recurrence rules and to render all-day dates. - `PORT` · required · Bound to localhost. There is no authentication because there is one user. - `TRASH_RETENTION_DAYS` · required · How long a deleted item stays recoverable before purge offers to remove it. ## 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 · Zenkit product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: One Node process serving server-rendered pages, no client framework, SQLite in WAL mode, one file, custom field values in typed columns, A documented subset of the iCalendar recurrence rule, stored as text and expanded on read, Instants in UTC for anything with a time, plain dates for all-day items, Whole workspace to JSON and any view to CSV, written in the first phase. - 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 - The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. - Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. - Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. - Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. - Copying a SQLite file with cp while the process is running can capture a database mid-transaction along with a stale write-ahead log, and the copy opens fine until the day you actually need it. Use the backup interface or a documented checkpoint, and restore one copy into a scratch directory as part of the routine rather than trusting it.
# Delivery milestones · Zenkit Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One item table that survives a new field A user adds a field to a list without a migration and without losing the ability to sort or filter on it. ### Steps 1. Give each value kind its own column One text column for everything is what makes sorting and filtering wrong later. Files: `src/db/schema.sql`, `src/fields.js` 2. Make field changes data rather than migrations The user invents fields faster than you ship releases. Files: `src/fields.js`, `tests/fields.test.js` 3. Test the sorts that text ordering gets wrong Nine against ten, and March against May. Files: `tests/sorting.test.js` ### Done when - [ ] Number and date fields sort by value, not as text - [ ] Adding a field runs no schema change - [ ] Empty, zero and the text 'null' stay distinct - [ ] Deleting a field keeps the underlying values ### Watch out - The obvious shape stores every custom value as text in one column, and then a number field sorts 10 before 9, a date filter compares strings so March sorts before May, and an empty value and the string 'null' become the same thing. Give the values table a column per kind, write into exactly one of them, and index the column the field actually uses. ## M2 · Views are saved queries, never second copies The same items shown as a table, a board and a calendar always agree. ### Steps 1. Store the filter, not the results A view that caches rows is a second truth that drifts by Friday. Files: `src/views.js` 2. Make a drag write one field The board is a grouping, so moving a card is an update. Files: `src/views.js`, `src/public/board.js` 3. Degrade a view whose field is gone Field deletion is common and should not produce a blank page. Files: `src/views.js`, `tests/views.test.js` ### Done when - [ ] A board drag updates the grouping field - [ ] No cards or columns table exists in the schema - [ ] A view with a deleted field explains itself - [ ] Ordering within a group survives a reload ### Watch out - Building the board as its own table of cards with their own positions means moving an item in the table view and moving it on the board write to two different places, and within a week the two views disagree about which column a task is in with no way to tell which is right. Make the board a grouping over the item rows, so dragging a card is an update to one field. ## M3 · Recurring items that survive February A repeating task generates the right dates across short months, clock changes and a completed occurrence. ### Steps 1. Expand from the rule, never from the last date Adding a month to a date is how the 31st becomes fiction. Files: `src/recurrence.js`, `tests/recurrence.test.js` 2. Drop invalid dates as the specification says Section 3.3.10 is explicit that they are ignored, not rounded. Files: `src/recurrence.js` 3. Separate one occurrence from the series Ticking Tuesday off should not end the habit. Files: `src/items.js`, `tests/occurrences.test.js` ### Done when - [ ] The 31st rule skips February and keeps March - [ ] A daily rule holds its local time across both clock changes - [ ] Count and end date together is rejected with a reason - [ ] Completing one occurrence leaves the series running ### Watch out - Generating the next date by adding one month to the previous one turns the 31st of January into an invented 31st of February, or quietly shifts a monthly task to the 28th forever once it lands there. Expand from the rule and the original start each time, and drop invalid dates the way the specification says rather than rounding them into the nearest real day. ## M4 · Capture in three seconds, and undo Adding a task is faster than opening a form, and every destructive action is reversible. ### Steps 1. Append every write to the activity table Before and after values, because undo has to know both. Files: `src/activity.js` 2. Make delete a timestamp A single-user app has no colleague with a copy. Files: `src/items.js`, `tests/trash.test.js` 3. Give capture a global shortcut Capture that needs navigation does not get used. Files: `src/public/capture.js` ### Done when - [ ] An item is created entirely from the keyboard - [ ] Undo restores a field value and a completion - [ ] Deleted items are recoverable from a trash view - [ ] Purge states the count before removing anything ### Watch out - Hard deleting an item on a mis-click is unrecoverable in a single-user app because there is nobody to ask for a copy, and the user discovers it a week later when the task never came back. Delete by timestamp, keep the row, and put purge behind its own screen with a count of what is about to go. ## M5 · Everything out, and back in again (production only) The workspace can leave this app intact and return without loss. ### Steps 1. Round-trip the export in a test An export nobody has imported is a guess about the future. Files: `src/export.js`, `tests/roundtrip.test.js` 2. Escape CSV against real values Commas, quotes and newlines live in every notes field. Files: `src/csv.js`, `tests/csv.test.js` 3. Back up through the backup interface Copying the file under load produces a copy that opens until it matters. Files: `src/backup.js`, `docs/restore.md` ### Done when - [ ] Export then import reproduces the workspace field by field - [ ] CSV survives commas, quotes and newlines - [ ] A backup under write load restores consistently - [ ] The README names the restore command ### Watch out - Copying a SQLite file with cp while the process is running can capture a database mid-transaction along with a stale write-ahead log, and the copy opens fine until the day you actually need it. Use the backup interface or a documented checkpoint, and restore one copy into a scratch directory as part of the routine rather than trusting it.
# Operations · Zenkit ## Backup Copy the database through the SQLite backup interface on a schedule, keep several generations, and hold one copy off the machine. A copy taken with cp under write load is the failure mode this avoids. ## Restore Restore into a scratch directory, open it, confirm the item count and a recurring item's next occurrence, then swap. Rehearse it once before you need it. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a single-user app the checks belong in the suite: the round-trip export, both clock change dates, the 31st rule, and a restore of a backup taken under load. ## Incident checklist The two incidents are a lost item and a wrong date. The activity log answers the first and names the write that did it; the recurrence tests answer the second, which is why the transition dates are in the suite rather than in a note. 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 - [ ] Number and date fields are confirmed to sort by value - [ ] A board drag is confirmed to write one field on the item - [ ] The monthly 31st rule is confirmed to skip February - [ ] Undo is confirmed to restore a field value and a completion - [ ] Export then import is confirmed to reproduce the workspace ## Launch constraint Do not market omitted Zenkit 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. The whole workspace. This is the file to back up. DATABASE_PATH=./data/work.db # Required. Where the backup interface writes copies, and where a restore is rehearsed from. BACKUP_DIR=./data/backups # Required. Used to expand recurrence rules and to render all-day dates. DEFAULT_TIMEZONE=Europe/London # Required. Bound to localhost. There is no authentication because there is one user. PORT=4180 # Required. How long a deleted item stays recoverable before purge offers to remove it. TRASH_RETENTION_DAYS=30
$ choose a build depth, inspect the files, then open the complete pack in your agent
Zenkit: Teams pay 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
Zenkit pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| plus | — | $9.24/user | Maximum 10 users; 100 workspaces; 50 lists; 20,000 items; 1,000 comments/seat/month; 300 automation runs/seat/month; 6 GB storage. |
| team | — | $13.86/user | Unlimited users; 1,000 workspaces; 200 lists; 100,000 items; 10,000 comments/seat/month; 3,000 automation runs/seat/month; 20 GB storage. |
| business | — | $24.26/user | 5,000 workspaces; 1,000 lists; 150,000 items; 10,000 comments/seat/month; 3,000 automation runs/seat/month; 50 GB storage. |
| enterprise | custom | — | Custom quotas; unlimited-quota option; dedicated success management and SLA. |
free tierno free tier
billingmonthly + annual (save up to 16%); exact monthly prices were not exposed by the live static page; 14-day trial
hidden costsEvery organization member, including owners and admins, is billable on paid plans; VAT is excluded; quota overages have a roughly 14-day grace period before restrictions; the post-trial account can become export-only.
verified 2026-08-14 · source ↗
Vibecode Zenkit
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Zenkit replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Zenkit cost?
Zenkit's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Zenkit?
Honestly: enterprise permissions; large integration catalog; mobile capture and notifications; real-time multiplayer. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Zenkit?
Yes: Vikunja (Open-source task manager with lists, boards, and collaboration.), Plane (Open-source issue and project tracker.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.