Vibecode Fibery
track this build4 phases, 12 steps, beginner friendly0%Fibery's solo core is compact: build a single-user connected work platform 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 Fibery. 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 ===== # Fibery · indie build A personal workspace whose hard problems are letting the user define types without the schema becoming unmanageable, links that stay honest when something is deleted, and recurrence rules that behave correctly on the thirty first of February. 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 | Next.js 15 with TypeScript, one process on localhost | one person, one machine, no coordination problem | | Storage | SQLite with a typed entity table plus a per-type field definition table | user-defined types need a schema about schemas, and this is the small version | | Recurrence | A documented subset of the iCalendar recurrence rule, expanded into concrete instances | borrowing a specification beats inventing one and then discovering February | | Views | Saved filters, sorts and groupings stored as data | a view somebody rebuilds every morning is not a view | | Export | One JSON document containing the schema and the data together | data without its schema is a pile of untyped values | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the seed scripts. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A recurrence library implementing the iCalendar rule** · free - Why: Recurrence is a specification with published edge cases, and every hand-rolled version gets short months or clock changes wrong in a way that only shows up months later. - Get it: A maintained library for your runtime that implements the recurrence rule and its exception dates. - Verify: A monthly rule on the thirty first skips short months in its output - [ ] **Current timezone data in your runtime** · free - Why: Recurrence across a clock change is decided by the zone rules for that date, and stale zone data produces tasks an hour out for weeks at a time. - Get it: Your runtime's bundled data, updated, or the system database. - Verify: Both of this year's transition dates resolve correctly in a test - [ ] **A seeded workspace at realistic size** · free - Why: The value-per-row design only reveals its cost above a certain number of entities, and that number is well above what a hand-made test workspace contains. - Get it: A script that generates tens of thousands of entities across several types. - Verify: A three-condition view over the seeded data returns within your stated limit - [ ] **The recurrence subset you will support** · free - Why: The full specification is large, and supporting half of it silently is worse than supporting a quarter of it loudly. - Get it: Write down which frequencies and modifiers you accept, and reject the rest at save time with a clear message. - Verify: An unsupported rule is refused when saved, naming the unsupported part ## 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: - Real-time multiplayer. - Permissions and roles. - An integration catalogue. - Whiteboards and collaborative documents. - 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 Fibery, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Fibery 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 tracker with types you define, links between them, and recurring work that respects the calendar to replace Fibery. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript, one process on localhost · one person, one machine, no coordination problem - Storage: SQLite with a typed entity table plus a per-type field definition table · user-defined types need a schema about schemas, and this is the small version - Recurrence: A documented subset of the iCalendar recurrence rule, expanded into concrete instances · borrowing a specification beats inventing one and then discovering February - Views: Saved filters, sorts and groupings stored as data · a view somebody rebuilds every morning is not a view - Export: One JSON document containing the schema and the data together · data without its schema is a pile of untyped values ### Data model (create this before Phase 1) - types(id, name, icon, created_at) · user-defined, because the point is that your work does not fit three fixed tables - fields(id, type_id, key, label, kind, options_json, required, ordinal, retired_at) · retired rather than deleted, so old values still have a definition - entities(id, type_id, created_at, updated_at, deleted_at) · one row for everything, typed by its type - values(entity_id, field_id, value_text, value_number, value_date) · one row per field per entity, which is slower to query and impossible to corrupt - relations(id, from_type_id, to_type_id, name, inverse_name, cardinality) and links(relation_id, from_entity_id, to_entity_id) · both directions from one row - recurrences(entity_id, rule_text, dtstart, timezone, until_or_count, exceptions_json) · the rule plus the exceptions, never a list of dates alone - views(id, name, type_id, filter_json, sort_json, group_by_field_id) · the saved question, which is most of what a workspace actually is ### Phase 1 · Types you can define without breaking yesterday Build: Let the user define types and fields at runtime with a stable key per field. Support text, number, date, select, checkbox and relation. Retire a field rather than deleting it, and keep its values readable. Validate values against the field kind on write. Trap: Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. Done when: Retiring a field hides it from forms and views while its stored values remain readable, tested by retiring and then querying. Renaming a field label changes no stored value, because values reference a stable key. Writing a value of the wrong kind is rejected with the field and expected kind named. Changing a field's kind is refused unless every existing value converts, and the refusal lists the values that do not. Do not build yet: No relations, views, recurrence or export. ### Phase 2 · Links that survive a deletion Build: Define relations between types with a name and an inverse name, and store one link row that serves both directions. Show backlinks on every entity. Decide and enforce what happens to links when an entity is deleted, and make the choice visible before the delete happens. Trap: Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. Done when: A link created from either side appears from both, tested in both directions. Deleting an entity either refuses while links exist or removes them explicitly, and the interface says which before the deletion happens. No link row can reference a missing entity, enforced by the database and asserted by a test that tries. Backlinks on an entity list every relation that points at it, with the inverse name shown. Do not build yet: No views, recurrence or export yet. ### Phase 3 · Saved views that stay fast Build: Store a view as a filter tree, a sort and an optional grouping. Compile it to a query rather than filtering in memory. Index the value table by field and by the typed columns, and measure a view over a realistic number of entities. Trap: A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. Done when: A view with three conditions and a sort returns within a measured time over a seeded workspace of tens of thousands of entities, and the figure is recorded in the README. Filtering happens in the query and not in application code, verified by inspecting the generated statement. A saved view reopens identically after a restart. An invalid filter, such as a date comparison on a text field, is refused when saved rather than at run time. Do not build yet: No recurrence or export yet. ### Phase 4 · Recurrence that survives February Build: Store a recurrence as a rule, a start instant and a timezone, plus explicit exceptions. Expand it into concrete instances over a bounded window rather than forever. Completing an instance never rewrites the rule, and editing the rule never deletes completed instances. Trap: A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. Done when: A monthly rule on the thirty first produces no instance in short months and the skipped months are not counted against a bounded series, tested across a full year. A daily rule across a clock change fires once per local day, tested on both transition dates. Completing an instance leaves the rule unchanged and the completion attached to that date. Editing a rule offers this instance or the whole series and never deletes a completed instance either way. Do not build yet: No export, backup or import yet. ### Phase 5 · A workspace that can leave Build: Export one JSON document holding types, fields including retired ones, relations, entities, values, recurrences and views. Import it into an empty database and compare. Back up the database file on a schedule and test a restore. Trap: Exporting entities and values without the field definitions produces a file full of keys nobody can interpret, and it is only discovered when somebody tries to import it somewhere, by which time the original workspace may be gone. Export the schema in the same document, including retired fields, and prove it by importing into an empty database. Done when: Exporting and importing into an empty database reproduces every type, field, relation, entity, value, recurrence and view, compared programmatically in the suite. Retired fields survive the round trip so their historical values remain interpretable. A restore from backup is exercised in the suite rather than described. The export is a single file that opens in a text editor and is readable without this application. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time multiplayer, which is the feature that makes Fibery a team product and the single largest jump in difficulty available here. - Enterprise permissions, since a workspace with one user has exactly one permission and pretending otherwise adds screens with no behaviour behind them. - An integration catalogue, where each connector is an authentication project plus an ongoing maintenance commitment. - Whiteboards and rich collaborative documents, which are each a substantial editor in their own right. ### README must contain - That types and fields are user-defined and that retiring a field keeps its values readable - The recurrence subset supported, and that invalid dates such as the thirty first of a short month are skipped rather than moved - The measured view query time at a stated workspace size, so a slow filter is a known limit - That export contains the schema and the data in one file and imports into an empty database - Where the database file is, how it is backed up, and that the restore is tested in the suite ===== AGENTS.md ===== # Agent instructions · Fibery indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript, one process on localhost, SQLite with a typed entity table plus a per-type field definition table, A documented subset of the iCalendar recurrence rule, expanded into concrete instances, Saved filters, sorts and groupings stored as data, One JSON document containing the schema and the data together. 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 - Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. - Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. - A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. - A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. ===== BUILD_PLAN.md ===== # Build plan · Fibery A personal workspace whose hard problems are letting the user define types without the schema becoming unmanageable, links that stay honest when something is deleted, and recurrence rules that behave correctly on the thirty first of February. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Types you can define without breaking yesterday Fields are added, renamed and retired while every existing value keeps its meaning. ### Steps 1. Stable keys, editable labels The same separation that makes a form builder survivable makes a workspace survivable. Files: `src/schema/fields.ts` 2. Retire, never silently delete Hidden and gone look identical in the interface and are not the same thing at all. Files: `src/schema/retire.ts`, `test/retire.test.ts` 3. Validate on write against the kind A typed store that accepts anything is an untyped store with extra tables. Files: `src/values/validate.ts` ### Done when - [ ] Retired fields keep readable values - [ ] Renaming a label changes no value - [ ] Wrong-kind values are rejected by name - [ ] A kind change is refused with the offending values listed ### Watch out - Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. ## Phase 2 · Links that survive a deletion Entities link in both directions and a deleted entity never leaves a dangling reference. ### Steps 1. One row, two directions Two rows for one relationship is two chances to disagree. Files: `src/relations/links.ts` 2. Let the database enforce both ends Foreign keys are free and your code is not. Files: `src/db/schema.sql`, `test/dangling.test.ts` 3. Say what a delete will do, first Deleting a project should never quietly orphan forty tasks. Files: `src/components/DeleteDialog.tsx` ### Done when - [ ] Links appear from both sides - [ ] Deletion policy is enforced and shown in advance - [ ] No link can reference a missing entity - [ ] Backlinks list every inbound relation ### Watch out - Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. ## Phase 3 · Saved views that stay fast Filters, sorts and groupings are stored as data and still answer quickly at real size. ### Steps 1. Compile the filter to a query In-memory filtering is fine until the day it is not, and that day arrives silently. Files: `src/views/compile.ts` 2. Seed a realistic workspace for tests Two hundred rows will validate a design that cannot work. Files: `scripts/seed-large.ts` 3. Validate filters when they are saved A broken view should fail in the editor, not in the morning. Files: `src/views/validate.ts` ### Done when - [ ] A three-condition view is measured at realistic size - [ ] Filtering is in the query, not in memory - [ ] Saved views survive a restart identically - [ ] Invalid filters are refused at save time ### Watch out - A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. ## Phase 4 · Recurrence that survives February Repeating work expands correctly on short months, clock changes and edited series. ### Steps 1. Follow the specification, do not clamp An invalid date is skipped, not moved, and this is the whole bug. Files: `src/recurrence/expand.ts`, `test/recurrence.test.ts` 2. Expand over a bounded window An infinite series materialised into rows is a full disk with a to-do list on it. Files: `src/recurrence/window.ts` 3. Keep completions attached to dates Editing a series should never erase what somebody already did. Files: `src/recurrence/instances.ts` ### Done when - [ ] A monthly rule on the thirty first skips short months - [ ] A daily rule fires once per local day across a clock change - [ ] Completing an instance leaves the rule unchanged - [ ] Editing a series preserves completed instances ### Watch out - A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. ## Not in this build - Real-time multiplayer. - Permissions and roles. - An integration catalogue. - Whiteboards and collaborative documents. ## After v1, if you want it - A calendar view over recurrence instances, which needs no new storage - A board grouped by any select field, since grouping already exists in the view model - Quick capture from a keyboard shortcut, which is where most personal workspaces live or die ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything. One file, backed up on a schedule. DATABASE_PATH=./data/workspace.db # Required. Localhost. A single-user workspace has no business on a network interface. APP_BIND=127.0.0.1:3000 # Required. Used for recurrence expansion. Stored per recurrence, not assumed globally. TIMEZONE=Europe/London # Required. How far ahead instances are materialised. Beyond this they are computed on demand. RECURRENCE_WINDOW_DAYS=180 # Required. Scheduled copies of the database file. The restore is tested in the suite. BACKUP_DIR=./data/backups # Required. Retention by count. A workspace file is small and keeping two weeks costs nothing. BACKUP_KEEP=14 # Required. A view slower than this logs a warning with the compiled query, so it is findable. VIEW_QUERY_BUDGET_MS=200
You are building a lean indie version of Fibery. 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 ===== # Fibery · indie build A personal workspace whose hard problems are letting the user define types without the schema becoming unmanageable, links that stay honest when something is deleted, and recurrence rules that behave correctly on the thirty first of February. 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 | Next.js 15 with TypeScript, one process on localhost | one person, one machine, no coordination problem | | Storage | SQLite with a typed entity table plus a per-type field definition table | user-defined types need a schema about schemas, and this is the small version | | Recurrence | A documented subset of the iCalendar recurrence rule, expanded into concrete instances | borrowing a specification beats inventing one and then discovering February | | Views | Saved filters, sorts and groupings stored as data | a view somebody rebuilds every morning is not a view | | Export | One JSON document containing the schema and the data together | data without its schema is a pile of untyped values | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the seed scripts. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A recurrence library implementing the iCalendar rule** · free - Why: Recurrence is a specification with published edge cases, and every hand-rolled version gets short months or clock changes wrong in a way that only shows up months later. - Get it: A maintained library for your runtime that implements the recurrence rule and its exception dates. - Verify: A monthly rule on the thirty first skips short months in its output - [ ] **Current timezone data in your runtime** · free - Why: Recurrence across a clock change is decided by the zone rules for that date, and stale zone data produces tasks an hour out for weeks at a time. - Get it: Your runtime's bundled data, updated, or the system database. - Verify: Both of this year's transition dates resolve correctly in a test - [ ] **A seeded workspace at realistic size** · free - Why: The value-per-row design only reveals its cost above a certain number of entities, and that number is well above what a hand-made test workspace contains. - Get it: A script that generates tens of thousands of entities across several types. - Verify: A three-condition view over the seeded data returns within your stated limit - [ ] **The recurrence subset you will support** · free - Why: The full specification is large, and supporting half of it silently is worse than supporting a quarter of it loudly. - Get it: Write down which frequencies and modifiers you accept, and reject the rest at save time with a clear message. - Verify: An unsupported rule is refused when saved, naming the unsupported part ## 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: - Real-time multiplayer. - Permissions and roles. - An integration catalogue. - Whiteboards and collaborative documents. - 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 Fibery, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Fibery 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 tracker with types you define, links between them, and recurring work that respects the calendar to replace Fibery. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript, one process on localhost · one person, one machine, no coordination problem - Storage: SQLite with a typed entity table plus a per-type field definition table · user-defined types need a schema about schemas, and this is the small version - Recurrence: A documented subset of the iCalendar recurrence rule, expanded into concrete instances · borrowing a specification beats inventing one and then discovering February - Views: Saved filters, sorts and groupings stored as data · a view somebody rebuilds every morning is not a view - Export: One JSON document containing the schema and the data together · data without its schema is a pile of untyped values ### Data model (create this before Phase 1) - types(id, name, icon, created_at) · user-defined, because the point is that your work does not fit three fixed tables - fields(id, type_id, key, label, kind, options_json, required, ordinal, retired_at) · retired rather than deleted, so old values still have a definition - entities(id, type_id, created_at, updated_at, deleted_at) · one row for everything, typed by its type - values(entity_id, field_id, value_text, value_number, value_date) · one row per field per entity, which is slower to query and impossible to corrupt - relations(id, from_type_id, to_type_id, name, inverse_name, cardinality) and links(relation_id, from_entity_id, to_entity_id) · both directions from one row - recurrences(entity_id, rule_text, dtstart, timezone, until_or_count, exceptions_json) · the rule plus the exceptions, never a list of dates alone - views(id, name, type_id, filter_json, sort_json, group_by_field_id) · the saved question, which is most of what a workspace actually is ### Phase 1 · Types you can define without breaking yesterday Build: Let the user define types and fields at runtime with a stable key per field. Support text, number, date, select, checkbox and relation. Retire a field rather than deleting it, and keep its values readable. Validate values against the field kind on write. Trap: Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. Done when: Retiring a field hides it from forms and views while its stored values remain readable, tested by retiring and then querying. Renaming a field label changes no stored value, because values reference a stable key. Writing a value of the wrong kind is rejected with the field and expected kind named. Changing a field's kind is refused unless every existing value converts, and the refusal lists the values that do not. Do not build yet: No relations, views, recurrence or export. ### Phase 2 · Links that survive a deletion Build: Define relations between types with a name and an inverse name, and store one link row that serves both directions. Show backlinks on every entity. Decide and enforce what happens to links when an entity is deleted, and make the choice visible before the delete happens. Trap: Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. Done when: A link created from either side appears from both, tested in both directions. Deleting an entity either refuses while links exist or removes them explicitly, and the interface says which before the deletion happens. No link row can reference a missing entity, enforced by the database and asserted by a test that tries. Backlinks on an entity list every relation that points at it, with the inverse name shown. Do not build yet: No views, recurrence or export yet. ### Phase 3 · Saved views that stay fast Build: Store a view as a filter tree, a sort and an optional grouping. Compile it to a query rather than filtering in memory. Index the value table by field and by the typed columns, and measure a view over a realistic number of entities. Trap: A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. Done when: A view with three conditions and a sort returns within a measured time over a seeded workspace of tens of thousands of entities, and the figure is recorded in the README. Filtering happens in the query and not in application code, verified by inspecting the generated statement. A saved view reopens identically after a restart. An invalid filter, such as a date comparison on a text field, is refused when saved rather than at run time. Do not build yet: No recurrence or export yet. ### Phase 4 · Recurrence that survives February Build: Store a recurrence as a rule, a start instant and a timezone, plus explicit exceptions. Expand it into concrete instances over a bounded window rather than forever. Completing an instance never rewrites the rule, and editing the rule never deletes completed instances. Trap: A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. Done when: A monthly rule on the thirty first produces no instance in short months and the skipped months are not counted against a bounded series, tested across a full year. A daily rule across a clock change fires once per local day, tested on both transition dates. Completing an instance leaves the rule unchanged and the completion attached to that date. Editing a rule offers this instance or the whole series and never deletes a completed instance either way. Do not build yet: No export, backup or import yet. ### Phase 5 · A workspace that can leave Build: Export one JSON document holding types, fields including retired ones, relations, entities, values, recurrences and views. Import it into an empty database and compare. Back up the database file on a schedule and test a restore. Trap: Exporting entities and values without the field definitions produces a file full of keys nobody can interpret, and it is only discovered when somebody tries to import it somewhere, by which time the original workspace may be gone. Export the schema in the same document, including retired fields, and prove it by importing into an empty database. Done when: Exporting and importing into an empty database reproduces every type, field, relation, entity, value, recurrence and view, compared programmatically in the suite. Retired fields survive the round trip so their historical values remain interpretable. A restore from backup is exercised in the suite rather than described. The export is a single file that opens in a text editor and is readable without this application. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time multiplayer, which is the feature that makes Fibery a team product and the single largest jump in difficulty available here. - Enterprise permissions, since a workspace with one user has exactly one permission and pretending otherwise adds screens with no behaviour behind them. - An integration catalogue, where each connector is an authentication project plus an ongoing maintenance commitment. - Whiteboards and rich collaborative documents, which are each a substantial editor in their own right. ### README must contain - That types and fields are user-defined and that retiring a field keeps its values readable - The recurrence subset supported, and that invalid dates such as the thirty first of a short month are skipped rather than moved - The measured view query time at a stated workspace size, so a slow filter is a known limit - That export contains the schema and the data in one file and imports into an empty database - Where the database file is, how it is backed up, and that the restore is tested in the suite ===== AGENTS.md ===== # Agent instructions · Fibery indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript, one process on localhost, SQLite with a typed entity table plus a per-type field definition table, A documented subset of the iCalendar recurrence rule, expanded into concrete instances, Saved filters, sorts and groupings stored as data, One JSON document containing the schema and the data together. 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 - Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. - Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. - A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. - A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. ===== BUILD_PLAN.md ===== # Build plan · Fibery A personal workspace whose hard problems are letting the user define types without the schema becoming unmanageable, links that stay honest when something is deleted, and recurrence rules that behave correctly on the thirty first of February. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Types you can define without breaking yesterday Fields are added, renamed and retired while every existing value keeps its meaning. ### Steps 1. Stable keys, editable labels The same separation that makes a form builder survivable makes a workspace survivable. Files: `src/schema/fields.ts` 2. Retire, never silently delete Hidden and gone look identical in the interface and are not the same thing at all. Files: `src/schema/retire.ts`, `test/retire.test.ts` 3. Validate on write against the kind A typed store that accepts anything is an untyped store with extra tables. Files: `src/values/validate.ts` ### Done when - [ ] Retired fields keep readable values - [ ] Renaming a label changes no value - [ ] Wrong-kind values are rejected by name - [ ] A kind change is refused with the offending values listed ### Watch out - Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. ## Phase 2 · Links that survive a deletion Entities link in both directions and a deleted entity never leaves a dangling reference. ### Steps 1. One row, two directions Two rows for one relationship is two chances to disagree. Files: `src/relations/links.ts` 2. Let the database enforce both ends Foreign keys are free and your code is not. Files: `src/db/schema.sql`, `test/dangling.test.ts` 3. Say what a delete will do, first Deleting a project should never quietly orphan forty tasks. Files: `src/components/DeleteDialog.tsx` ### Done when - [ ] Links appear from both sides - [ ] Deletion policy is enforced and shown in advance - [ ] No link can reference a missing entity - [ ] Backlinks list every inbound relation ### Watch out - Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. ## Phase 3 · Saved views that stay fast Filters, sorts and groupings are stored as data and still answer quickly at real size. ### Steps 1. Compile the filter to a query In-memory filtering is fine until the day it is not, and that day arrives silently. Files: `src/views/compile.ts` 2. Seed a realistic workspace for tests Two hundred rows will validate a design that cannot work. Files: `scripts/seed-large.ts` 3. Validate filters when they are saved A broken view should fail in the editor, not in the morning. Files: `src/views/validate.ts` ### Done when - [ ] A three-condition view is measured at realistic size - [ ] Filtering is in the query, not in memory - [ ] Saved views survive a restart identically - [ ] Invalid filters are refused at save time ### Watch out - A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. ## Phase 4 · Recurrence that survives February Repeating work expands correctly on short months, clock changes and edited series. ### Steps 1. Follow the specification, do not clamp An invalid date is skipped, not moved, and this is the whole bug. Files: `src/recurrence/expand.ts`, `test/recurrence.test.ts` 2. Expand over a bounded window An infinite series materialised into rows is a full disk with a to-do list on it. Files: `src/recurrence/window.ts` 3. Keep completions attached to dates Editing a series should never erase what somebody already did. Files: `src/recurrence/instances.ts` ### Done when - [ ] A monthly rule on the thirty first skips short months - [ ] A daily rule fires once per local day across a clock change - [ ] Completing an instance leaves the rule unchanged - [ ] Editing a series preserves completed instances ### Watch out - A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. ## Not in this build - Real-time multiplayer. - Permissions and roles. - An integration catalogue. - Whiteboards and collaborative documents. ## After v1, if you want it - A calendar view over recurrence instances, which needs no new storage - A board grouped by any select field, since grouping already exists in the view model - Quick capture from a keyboard shortcut, which is where most personal workspaces live or die ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything. One file, backed up on a schedule. DATABASE_PATH=./data/workspace.db # Required. Localhost. A single-user workspace has no business on a network interface. APP_BIND=127.0.0.1:3000 # Required. Used for recurrence expansion. Stored per recurrence, not assumed globally. TIMEZONE=Europe/London # Required. How far ahead instances are materialised. Beyond this they are computed on demand. RECURRENCE_WINDOW_DAYS=180 # Required. Scheduled copies of the database file. The restore is tested in the suite. BACKUP_DIR=./data/backups # Required. Retention by count. A workspace file is small and keeping two weeks costs nothing. BACKUP_KEEP=14 # Required. A view slower than this logs a warning with the compiled query, so it is findable. VIEW_QUERY_BUDGET_MS=200
You are building a production product version of Fibery. 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 ===== # Fibery · product brief ## Problem Fibery's solo core is compact: build a single-user connected work platform 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 personal workspace whose schema can change without losing meaning, whose links cannot dangle, whose views still answer at real size, and whose recurring tasks do not invent a thirtieth of February. ## 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 - Real-time multiplayer. - Permissions and roles. - An integration catalogue. - Whiteboards and collaborative documents. - enterprise permissions - large integration catalog - mobile capture and notifications - real-time multiplayer ## Success criteria - Retired fields are confirmed to keep readable values - A link is confirmed unable to reference a missing entity - A three-condition view is confirmed measured at realistic size - A monthly rule on the thirty first is confirmed to skip short months - An export is confirmed to round trip into an empty database ===== BRIEF.md ===== # Build brief · Fibery 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 tracker with types you define, links between them, and recurring work that respects the calendar to replace Fibery. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript, one process on localhost · one person, one machine, no coordination problem - Storage: SQLite with a typed entity table plus a per-type field definition table · user-defined types need a schema about schemas, and this is the small version - Recurrence: A documented subset of the iCalendar recurrence rule, expanded into concrete instances · borrowing a specification beats inventing one and then discovering February - Views: Saved filters, sorts and groupings stored as data · a view somebody rebuilds every morning is not a view - Export: One JSON document containing the schema and the data together · data without its schema is a pile of untyped values ### Data model (create this before Phase 1) - types(id, name, icon, created_at) · user-defined, because the point is that your work does not fit three fixed tables - fields(id, type_id, key, label, kind, options_json, required, ordinal, retired_at) · retired rather than deleted, so old values still have a definition - entities(id, type_id, created_at, updated_at, deleted_at) · one row for everything, typed by its type - values(entity_id, field_id, value_text, value_number, value_date) · one row per field per entity, which is slower to query and impossible to corrupt - relations(id, from_type_id, to_type_id, name, inverse_name, cardinality) and links(relation_id, from_entity_id, to_entity_id) · both directions from one row - recurrences(entity_id, rule_text, dtstart, timezone, until_or_count, exceptions_json) · the rule plus the exceptions, never a list of dates alone - views(id, name, type_id, filter_json, sort_json, group_by_field_id) · the saved question, which is most of what a workspace actually is ### Phase 1 · Types you can define without breaking yesterday Build: Let the user define types and fields at runtime with a stable key per field. Support text, number, date, select, checkbox and relation. Retire a field rather than deleting it, and keep its values readable. Validate values against the field kind on write. Trap: Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. Done when: Retiring a field hides it from forms and views while its stored values remain readable, tested by retiring and then querying. Renaming a field label changes no stored value, because values reference a stable key. Writing a value of the wrong kind is rejected with the field and expected kind named. Changing a field's kind is refused unless every existing value converts, and the refusal lists the values that do not. Do not build yet: No relations, views, recurrence or export. ### Phase 2 · Links that survive a deletion Build: Define relations between types with a name and an inverse name, and store one link row that serves both directions. Show backlinks on every entity. Decide and enforce what happens to links when an entity is deleted, and make the choice visible before the delete happens. Trap: Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. Done when: A link created from either side appears from both, tested in both directions. Deleting an entity either refuses while links exist or removes them explicitly, and the interface says which before the deletion happens. No link row can reference a missing entity, enforced by the database and asserted by a test that tries. Backlinks on an entity list every relation that points at it, with the inverse name shown. Do not build yet: No views, recurrence or export yet. ### Phase 3 · Saved views that stay fast Build: Store a view as a filter tree, a sort and an optional grouping. Compile it to a query rather than filtering in memory. Index the value table by field and by the typed columns, and measure a view over a realistic number of entities. Trap: A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. Done when: A view with three conditions and a sort returns within a measured time over a seeded workspace of tens of thousands of entities, and the figure is recorded in the README. Filtering happens in the query and not in application code, verified by inspecting the generated statement. A saved view reopens identically after a restart. An invalid filter, such as a date comparison on a text field, is refused when saved rather than at run time. Do not build yet: No recurrence or export yet. ### Phase 4 · Recurrence that survives February Build: Store a recurrence as a rule, a start instant and a timezone, plus explicit exceptions. Expand it into concrete instances over a bounded window rather than forever. Completing an instance never rewrites the rule, and editing the rule never deletes completed instances. Trap: A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. Done when: A monthly rule on the thirty first produces no instance in short months and the skipped months are not counted against a bounded series, tested across a full year. A daily rule across a clock change fires once per local day, tested on both transition dates. Completing an instance leaves the rule unchanged and the completion attached to that date. Editing a rule offers this instance or the whole series and never deletes a completed instance either way. Do not build yet: No export, backup or import yet. ### Phase 5 · A workspace that can leave Build: Export one JSON document holding types, fields including retired ones, relations, entities, values, recurrences and views. Import it into an empty database and compare. Back up the database file on a schedule and test a restore. Trap: Exporting entities and values without the field definitions produces a file full of keys nobody can interpret, and it is only discovered when somebody tries to import it somewhere, by which time the original workspace may be gone. Export the schema in the same document, including retired fields, and prove it by importing into an empty database. Done when: Exporting and importing into an empty database reproduces every type, field, relation, entity, value, recurrence and view, compared programmatically in the suite. Retired fields survive the round trip so their historical values remain interpretable. A restore from backup is exercised in the suite rather than described. The export is a single file that opens in a text editor and is readable without this application. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time multiplayer, which is the feature that makes Fibery a team product and the single largest jump in difficulty available here. - Enterprise permissions, since a workspace with one user has exactly one permission and pretending otherwise adds screens with no behaviour behind them. - An integration catalogue, where each connector is an authentication project plus an ongoing maintenance commitment. - Whiteboards and rich collaborative documents, which are each a substantial editor in their own right. ### README must contain - That types and fields are user-defined and that retiring a field keeps its values readable - The recurrence subset supported, and that invalid dates such as the thirty first of a short month are skipped rather than moved - The measured view query time at a stated workspace size, so a slow filter is a known limit - That export contains the schema and the data in one file and imports into an empty database - Where the database file is, how it is backed up, and that the restore is tested in the suite ===== ARCHITECTURE.md ===== # Architecture · Fibery ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript, one process on localhost | one person, one machine, no coordination problem | | Storage | SQLite with a typed entity table plus a per-type field definition table | user-defined types need a schema about schemas, and this is the small version | | Recurrence | A documented subset of the iCalendar recurrence rule, expanded into concrete instances | borrowing a specification beats inventing one and then discovering February | | Views | Saved filters, sorts and groupings stored as data | a view somebody rebuilds every morning is not a view | | Export | One JSON document containing the schema and the data together | data without its schema is a pile of untyped values | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Schema | Types, fields and retirement. | Any store, provided retiring keeps values readable. | | Relations | One link row serving both directions. | Any representation, provided both ends are enforced. | | Views | Filters compiled to queries. | Any query builder, provided filtering never moves into memory. | | Recurrence | Rule expansion following the specification. | Any library implementing the same rule; never a hand-rolled month calculation. | | Export | Schema and data in one document. | Any format, provided it round trips into an empty database. | ## 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 · Everything. One file, backed up on a schedule. - `APP_BIND` · required · Localhost. A single-user workspace has no business on a network interface. - `TIMEZONE` · required · Used for recurrence expansion. Stored per recurrence, not assumed globally. - `RECURRENCE_WINDOW_DAYS` · required · How far ahead instances are materialised. Beyond this they are computed on demand. - `BACKUP_DIR` · required · Scheduled copies of the database file. The restore is tested in the suite. - `BACKUP_KEEP` · required · Retention by count. A workspace file is small and keeping two weeks costs nothing. - `VIEW_QUERY_BUDGET_MS` · required · A view slower than this logs a warning with the compiled query, so it is findable. ## 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 · Fibery product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 with TypeScript, one process on localhost, SQLite with a typed entity table plus a per-type field definition table, A documented subset of the iCalendar recurrence rule, expanded into concrete instances, Saved filters, sorts and groupings stored as data, One JSON document containing the schema and the data together. - 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 - Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. - Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. - A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. - A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. - Exporting entities and values without the field definitions produces a file full of keys nobody can interpret, and it is only discovered when somebody tries to import it somewhere, by which time the original workspace may be gone. Export the schema in the same document, including retired fields, and prove it by importing into an empty database. ===== MILESTONES.md ===== # Delivery milestones · Fibery Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Types you can define without breaking yesterday Fields are added, renamed and retired while every existing value keeps its meaning. ### Steps 1. Stable keys, editable labels The same separation that makes a form builder survivable makes a workspace survivable. Files: `src/schema/fields.ts` 2. Retire, never silently delete Hidden and gone look identical in the interface and are not the same thing at all. Files: `src/schema/retire.ts`, `test/retire.test.ts` 3. Validate on write against the kind A typed store that accepts anything is an untyped store with extra tables. Files: `src/values/validate.ts` ### Done when - [ ] Retired fields keep readable values - [ ] Renaming a label changes no value - [ ] Wrong-kind values are rejected by name - [ ] A kind change is refused with the offending values listed ### Watch out - Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. ## M2 · Links that survive a deletion Entities link in both directions and a deleted entity never leaves a dangling reference. ### Steps 1. One row, two directions Two rows for one relationship is two chances to disagree. Files: `src/relations/links.ts` 2. Let the database enforce both ends Foreign keys are free and your code is not. Files: `src/db/schema.sql`, `test/dangling.test.ts` 3. Say what a delete will do, first Deleting a project should never quietly orphan forty tasks. Files: `src/components/DeleteDialog.tsx` ### Done when - [ ] Links appear from both sides - [ ] Deletion policy is enforced and shown in advance - [ ] No link can reference a missing entity - [ ] Backlinks list every inbound relation ### Watch out - Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. ## M3 · Saved views that stay fast Filters, sorts and groupings are stored as data and still answer quickly at real size. ### Steps 1. Compile the filter to a query In-memory filtering is fine until the day it is not, and that day arrives silently. Files: `src/views/compile.ts` 2. Seed a realistic workspace for tests Two hundred rows will validate a design that cannot work. Files: `scripts/seed-large.ts` 3. Validate filters when they are saved A broken view should fail in the editor, not in the morning. Files: `src/views/validate.ts` ### Done when - [ ] A three-condition view is measured at realistic size - [ ] Filtering is in the query, not in memory - [ ] Saved views survive a restart identically - [ ] Invalid filters are refused at save time ### Watch out - A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. ## M4 · Recurrence that survives February Repeating work expands correctly on short months, clock changes and edited series. ### Steps 1. Follow the specification, do not clamp An invalid date is skipped, not moved, and this is the whole bug. Files: `src/recurrence/expand.ts`, `test/recurrence.test.ts` 2. Expand over a bounded window An infinite series materialised into rows is a full disk with a to-do list on it. Files: `src/recurrence/window.ts` 3. Keep completions attached to dates Editing a series should never erase what somebody already did. Files: `src/recurrence/instances.ts` ### Done when - [ ] A monthly rule on the thirty first skips short months - [ ] A daily rule fires once per local day across a clock change - [ ] Completing an instance leaves the rule unchanged - [ ] Editing a series preserves completed instances ### Watch out - A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. ## M5 · A workspace that can leave (production only) Export carries the schema with the data and importing it reproduces the workspace exactly. ### Steps 1. Export the schema with the data Values without their field definitions are a puzzle, not a backup. Files: `src/export/workspace.ts` 2. Round trip into an empty database in the suite The only export test that means anything. Files: `test/roundtrip.test.ts` 3. Test the restore, not just the backup A backup nobody has restored is a file of unknown quality. Files: `scripts/backup.sh`, `test/restore.test.ts` ### Done when - [ ] A round trip reproduces the workspace exactly - [ ] Retired fields survive the export - [ ] Restore is exercised in the suite - [ ] The export file is readable without the app ### Watch out - Exporting entities and values without the field definitions produces a file full of keys nobody can interpret, and it is only discovered when somebody tries to import it somewhere, by which time the original workspace may be gone. Export the schema in the same document, including retired fields, and prove it by importing into an empty database. ===== OPERATIONS.md ===== # Operations · Fibery ## Backup The single database file on a schedule, with retention by count. It is small enough that keeping a fortnight costs nothing and losing a fortnight costs a workspace. ## Restore Restore into an empty directory, open the application, and check the entity count and a saved view against what you expect. The round trip test covers the format; this drill covers the procedure. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Views exceeding the query budget, the count of materialised recurrence instances, and backup age. A growing instance count usually means a rule with no end that nobody noticed writing rows. ## Incident checklist A missing task is nearly always a view filter rather than lost data, and the fastest check is querying the entity directly. A wrong recurrence date is a specification question, and the suite already contains the answer for short months and clock changes. 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 - [ ] Retired fields are confirmed to keep readable values - [ ] A link is confirmed unable to reference a missing entity - [ ] A three-condition view is confirmed measured at realistic size - [ ] A monthly rule on the thirty first is confirmed to skip short months - [ ] An export is confirmed to round trip into an empty database ## Launch constraint Do not market omitted Fibery 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. Everything. One file, backed up on a schedule. DATABASE_PATH=./data/workspace.db # Required. Localhost. A single-user workspace has no business on a network interface. APP_BIND=127.0.0.1:3000 # Required. Used for recurrence expansion. Stored per recurrence, not assumed globally. TIMEZONE=Europe/London # Required. How far ahead instances are materialised. Beyond this they are computed on demand. RECURRENCE_WINDOW_DAYS=180 # Required. Scheduled copies of the database file. The restore is tested in the suite. BACKUP_DIR=./data/backups # Required. Retention by count. A workspace file is small and keeping two weeks costs nothing. BACKUP_KEEP=14 # Required. A view slower than this logs a warning with the compiled query, so it is findable. VIEW_QUERY_BUDGET_MS=200
# Fibery · indie build A personal workspace whose hard problems are letting the user define types without the schema becoming unmanageable, links that stay honest when something is deleted, and recurrence rules that behave correctly on the thirty first of February. 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 | Next.js 15 with TypeScript, one process on localhost | one person, one machine, no coordination problem | | Storage | SQLite with a typed entity table plus a per-type field definition table | user-defined types need a schema about schemas, and this is the small version | | Recurrence | A documented subset of the iCalendar recurrence rule, expanded into concrete instances | borrowing a specification beats inventing one and then discovering February | | Views | Saved filters, sorts and groupings stored as data | a view somebody rebuilds every morning is not a view | | Export | One JSON document containing the schema and the data together | data without its schema is a pile of untyped values | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application and the seed scripts. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A recurrence library implementing the iCalendar rule** · free - Why: Recurrence is a specification with published edge cases, and every hand-rolled version gets short months or clock changes wrong in a way that only shows up months later. - Get it: A maintained library for your runtime that implements the recurrence rule and its exception dates. - Verify: A monthly rule on the thirty first skips short months in its output - [ ] **Current timezone data in your runtime** · free - Why: Recurrence across a clock change is decided by the zone rules for that date, and stale zone data produces tasks an hour out for weeks at a time. - Get it: Your runtime's bundled data, updated, or the system database. - Verify: Both of this year's transition dates resolve correctly in a test - [ ] **A seeded workspace at realistic size** · free - Why: The value-per-row design only reveals its cost above a certain number of entities, and that number is well above what a hand-made test workspace contains. - Get it: A script that generates tens of thousands of entities across several types. - Verify: A three-condition view over the seeded data returns within your stated limit - [ ] **The recurrence subset you will support** · free - Why: The full specification is large, and supporting half of it silently is worse than supporting a quarter of it loudly. - Get it: Write down which frequencies and modifiers you accept, and reject the rest at save time with a clear message. - Verify: An unsupported rule is refused when saved, naming the unsupported part ## 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: - Real-time multiplayer. - Permissions and roles. - An integration catalogue. - Whiteboards and collaborative documents. - 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 Fibery, and the README should say so rather than pretend.
# Build brief · Fibery 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 tracker with types you define, links between them, and recurring work that respects the calendar to replace Fibery. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript, one process on localhost · one person, one machine, no coordination problem - Storage: SQLite with a typed entity table plus a per-type field definition table · user-defined types need a schema about schemas, and this is the small version - Recurrence: A documented subset of the iCalendar recurrence rule, expanded into concrete instances · borrowing a specification beats inventing one and then discovering February - Views: Saved filters, sorts and groupings stored as data · a view somebody rebuilds every morning is not a view - Export: One JSON document containing the schema and the data together · data without its schema is a pile of untyped values ### Data model (create this before Phase 1) - types(id, name, icon, created_at) · user-defined, because the point is that your work does not fit three fixed tables - fields(id, type_id, key, label, kind, options_json, required, ordinal, retired_at) · retired rather than deleted, so old values still have a definition - entities(id, type_id, created_at, updated_at, deleted_at) · one row for everything, typed by its type - values(entity_id, field_id, value_text, value_number, value_date) · one row per field per entity, which is slower to query and impossible to corrupt - relations(id, from_type_id, to_type_id, name, inverse_name, cardinality) and links(relation_id, from_entity_id, to_entity_id) · both directions from one row - recurrences(entity_id, rule_text, dtstart, timezone, until_or_count, exceptions_json) · the rule plus the exceptions, never a list of dates alone - views(id, name, type_id, filter_json, sort_json, group_by_field_id) · the saved question, which is most of what a workspace actually is ### Phase 1 · Types you can define without breaking yesterday Build: Let the user define types and fields at runtime with a stable key per field. Support text, number, date, select, checkbox and relation. Retire a field rather than deleting it, and keep its values readable. Validate values against the field kind on write. Trap: Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. Done when: Retiring a field hides it from forms and views while its stored values remain readable, tested by retiring and then querying. Renaming a field label changes no stored value, because values reference a stable key. Writing a value of the wrong kind is rejected with the field and expected kind named. Changing a field's kind is refused unless every existing value converts, and the refusal lists the values that do not. Do not build yet: No relations, views, recurrence or export. ### Phase 2 · Links that survive a deletion Build: Define relations between types with a name and an inverse name, and store one link row that serves both directions. Show backlinks on every entity. Decide and enforce what happens to links when an entity is deleted, and make the choice visible before the delete happens. Trap: Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. Done when: A link created from either side appears from both, tested in both directions. Deleting an entity either refuses while links exist or removes them explicitly, and the interface says which before the deletion happens. No link row can reference a missing entity, enforced by the database and asserted by a test that tries. Backlinks on an entity list every relation that points at it, with the inverse name shown. Do not build yet: No views, recurrence or export yet. ### Phase 3 · Saved views that stay fast Build: Store a view as a filter tree, a sort and an optional grouping. Compile it to a query rather than filtering in memory. Index the value table by field and by the typed columns, and measure a view over a realistic number of entities. Trap: A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. Done when: A view with three conditions and a sort returns within a measured time over a seeded workspace of tens of thousands of entities, and the figure is recorded in the README. Filtering happens in the query and not in application code, verified by inspecting the generated statement. A saved view reopens identically after a restart. An invalid filter, such as a date comparison on a text field, is refused when saved rather than at run time. Do not build yet: No recurrence or export yet. ### Phase 4 · Recurrence that survives February Build: Store a recurrence as a rule, a start instant and a timezone, plus explicit exceptions. Expand it into concrete instances over a bounded window rather than forever. Completing an instance never rewrites the rule, and editing the rule never deletes completed instances. Trap: A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. Done when: A monthly rule on the thirty first produces no instance in short months and the skipped months are not counted against a bounded series, tested across a full year. A daily rule across a clock change fires once per local day, tested on both transition dates. Completing an instance leaves the rule unchanged and the completion attached to that date. Editing a rule offers this instance or the whole series and never deletes a completed instance either way. Do not build yet: No export, backup or import yet. ### Phase 5 · A workspace that can leave Build: Export one JSON document holding types, fields including retired ones, relations, entities, values, recurrences and views. Import it into an empty database and compare. Back up the database file on a schedule and test a restore. Trap: Exporting entities and values without the field definitions produces a file full of keys nobody can interpret, and it is only discovered when somebody tries to import it somewhere, by which time the original workspace may be gone. Export the schema in the same document, including retired fields, and prove it by importing into an empty database. Done when: Exporting and importing into an empty database reproduces every type, field, relation, entity, value, recurrence and view, compared programmatically in the suite. Retired fields survive the round trip so their historical values remain interpretable. A restore from backup is exercised in the suite rather than described. The export is a single file that opens in a text editor and is readable without this application. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time multiplayer, which is the feature that makes Fibery a team product and the single largest jump in difficulty available here. - Enterprise permissions, since a workspace with one user has exactly one permission and pretending otherwise adds screens with no behaviour behind them. - An integration catalogue, where each connector is an authentication project plus an ongoing maintenance commitment. - Whiteboards and rich collaborative documents, which are each a substantial editor in their own right. ### README must contain - That types and fields are user-defined and that retiring a field keeps its values readable - The recurrence subset supported, and that invalid dates such as the thirty first of a short month are skipped rather than moved - The measured view query time at a stated workspace size, so a slow filter is a known limit - That export contains the schema and the data in one file and imports into an empty database - Where the database file is, how it is backed up, and that the restore is tested in the suite
# Agent instructions · Fibery indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript, one process on localhost, SQLite with a typed entity table plus a per-type field definition table, A documented subset of the iCalendar recurrence rule, expanded into concrete instances, Saved filters, sorts and groupings stored as data, One JSON document containing the schema and the data together. 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 - Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. - Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. - A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. - A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month.
# Build plan · Fibery A personal workspace whose hard problems are letting the user define types without the schema becoming unmanageable, links that stay honest when something is deleted, and recurrence rules that behave correctly on the thirty first of February. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Types you can define without breaking yesterday Fields are added, renamed and retired while every existing value keeps its meaning. ### Steps 1. Stable keys, editable labels The same separation that makes a form builder survivable makes a workspace survivable. Files: `src/schema/fields.ts` 2. Retire, never silently delete Hidden and gone look identical in the interface and are not the same thing at all. Files: `src/schema/retire.ts`, `test/retire.test.ts` 3. Validate on write against the kind A typed store that accepts anything is an untyped store with extra tables. Files: `src/values/validate.ts` ### Done when - [ ] Retired fields keep readable values - [ ] Renaming a label changes no value - [ ] Wrong-kind values are rejected by name - [ ] A kind change is refused with the offending values listed ### Watch out - Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. ## Phase 2 · Links that survive a deletion Entities link in both directions and a deleted entity never leaves a dangling reference. ### Steps 1. One row, two directions Two rows for one relationship is two chances to disagree. Files: `src/relations/links.ts` 2. Let the database enforce both ends Foreign keys are free and your code is not. Files: `src/db/schema.sql`, `test/dangling.test.ts` 3. Say what a delete will do, first Deleting a project should never quietly orphan forty tasks. Files: `src/components/DeleteDialog.tsx` ### Done when - [ ] Links appear from both sides - [ ] Deletion policy is enforced and shown in advance - [ ] No link can reference a missing entity - [ ] Backlinks list every inbound relation ### Watch out - Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. ## Phase 3 · Saved views that stay fast Filters, sorts and groupings are stored as data and still answer quickly at real size. ### Steps 1. Compile the filter to a query In-memory filtering is fine until the day it is not, and that day arrives silently. Files: `src/views/compile.ts` 2. Seed a realistic workspace for tests Two hundred rows will validate a design that cannot work. Files: `scripts/seed-large.ts` 3. Validate filters when they are saved A broken view should fail in the editor, not in the morning. Files: `src/views/validate.ts` ### Done when - [ ] A three-condition view is measured at realistic size - [ ] Filtering is in the query, not in memory - [ ] Saved views survive a restart identically - [ ] Invalid filters are refused at save time ### Watch out - A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. ## Phase 4 · Recurrence that survives February Repeating work expands correctly on short months, clock changes and edited series. ### Steps 1. Follow the specification, do not clamp An invalid date is skipped, not moved, and this is the whole bug. Files: `src/recurrence/expand.ts`, `test/recurrence.test.ts` 2. Expand over a bounded window An infinite series materialised into rows is a full disk with a to-do list on it. Files: `src/recurrence/window.ts` 3. Keep completions attached to dates Editing a series should never erase what somebody already did. Files: `src/recurrence/instances.ts` ### Done when - [ ] A monthly rule on the thirty first skips short months - [ ] A daily rule fires once per local day across a clock change - [ ] Completing an instance leaves the rule unchanged - [ ] Editing a series preserves completed instances ### Watch out - A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. ## Not in this build - Real-time multiplayer. - Permissions and roles. - An integration catalogue. - Whiteboards and collaborative documents. ## After v1, if you want it - A calendar view over recurrence instances, which needs no new storage - A board grouped by any select field, since grouping already exists in the view model - Quick capture from a keyboard shortcut, which is where most personal workspaces live or die
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything. One file, backed up on a schedule. DATABASE_PATH=./data/workspace.db # Required. Localhost. A single-user workspace has no business on a network interface. APP_BIND=127.0.0.1:3000 # Required. Used for recurrence expansion. Stored per recurrence, not assumed globally. TIMEZONE=Europe/London # Required. How far ahead instances are materialised. Beyond this they are computed on demand. RECURRENCE_WINDOW_DAYS=180 # Required. Scheduled copies of the database file. The restore is tested in the suite. BACKUP_DIR=./data/backups # Required. Retention by count. A workspace file is small and keeping two weeks costs nothing. BACKUP_KEEP=14 # Required. A view slower than this logs a warning with the compiled query, so it is findable. VIEW_QUERY_BUDGET_MS=200
# Fibery · product brief ## Problem Fibery's solo core is compact: build a single-user connected work platform 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 personal workspace whose schema can change without losing meaning, whose links cannot dangle, whose views still answer at real size, and whose recurring tasks do not invent a thirtieth of February. ## 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 - Real-time multiplayer. - Permissions and roles. - An integration catalogue. - Whiteboards and collaborative documents. - enterprise permissions - large integration catalog - mobile capture and notifications - real-time multiplayer ## Success criteria - Retired fields are confirmed to keep readable values - A link is confirmed unable to reference a missing entity - A three-condition view is confirmed measured at realistic size - A monthly rule on the thirty first is confirmed to skip short months - An export is confirmed to round trip into an empty database
# Build brief · Fibery 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 tracker with types you define, links between them, and recurring work that respects the calendar to replace Fibery. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript, one process on localhost · one person, one machine, no coordination problem - Storage: SQLite with a typed entity table plus a per-type field definition table · user-defined types need a schema about schemas, and this is the small version - Recurrence: A documented subset of the iCalendar recurrence rule, expanded into concrete instances · borrowing a specification beats inventing one and then discovering February - Views: Saved filters, sorts and groupings stored as data · a view somebody rebuilds every morning is not a view - Export: One JSON document containing the schema and the data together · data without its schema is a pile of untyped values ### Data model (create this before Phase 1) - types(id, name, icon, created_at) · user-defined, because the point is that your work does not fit three fixed tables - fields(id, type_id, key, label, kind, options_json, required, ordinal, retired_at) · retired rather than deleted, so old values still have a definition - entities(id, type_id, created_at, updated_at, deleted_at) · one row for everything, typed by its type - values(entity_id, field_id, value_text, value_number, value_date) · one row per field per entity, which is slower to query and impossible to corrupt - relations(id, from_type_id, to_type_id, name, inverse_name, cardinality) and links(relation_id, from_entity_id, to_entity_id) · both directions from one row - recurrences(entity_id, rule_text, dtstart, timezone, until_or_count, exceptions_json) · the rule plus the exceptions, never a list of dates alone - views(id, name, type_id, filter_json, sort_json, group_by_field_id) · the saved question, which is most of what a workspace actually is ### Phase 1 · Types you can define without breaking yesterday Build: Let the user define types and fields at runtime with a stable key per field. Support text, number, date, select, checkbox and relation. Retire a field rather than deleting it, and keep its values readable. Validate values against the field kind on write. Trap: Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. Done when: Retiring a field hides it from forms and views while its stored values remain readable, tested by retiring and then querying. Renaming a field label changes no stored value, because values reference a stable key. Writing a value of the wrong kind is rejected with the field and expected kind named. Changing a field's kind is refused unless every existing value converts, and the refusal lists the values that do not. Do not build yet: No relations, views, recurrence or export. ### Phase 2 · Links that survive a deletion Build: Define relations between types with a name and an inverse name, and store one link row that serves both directions. Show backlinks on every entity. Decide and enforce what happens to links when an entity is deleted, and make the choice visible before the delete happens. Trap: Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. Done when: A link created from either side appears from both, tested in both directions. Deleting an entity either refuses while links exist or removes them explicitly, and the interface says which before the deletion happens. No link row can reference a missing entity, enforced by the database and asserted by a test that tries. Backlinks on an entity list every relation that points at it, with the inverse name shown. Do not build yet: No views, recurrence or export yet. ### Phase 3 · Saved views that stay fast Build: Store a view as a filter tree, a sort and an optional grouping. Compile it to a query rather than filtering in memory. Index the value table by field and by the typed columns, and measure a view over a realistic number of entities. Trap: A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. Done when: A view with three conditions and a sort returns within a measured time over a seeded workspace of tens of thousands of entities, and the figure is recorded in the README. Filtering happens in the query and not in application code, verified by inspecting the generated statement. A saved view reopens identically after a restart. An invalid filter, such as a date comparison on a text field, is refused when saved rather than at run time. Do not build yet: No recurrence or export yet. ### Phase 4 · Recurrence that survives February Build: Store a recurrence as a rule, a start instant and a timezone, plus explicit exceptions. Expand it into concrete instances over a bounded window rather than forever. Completing an instance never rewrites the rule, and editing the rule never deletes completed instances. Trap: A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. Done when: A monthly rule on the thirty first produces no instance in short months and the skipped months are not counted against a bounded series, tested across a full year. A daily rule across a clock change fires once per local day, tested on both transition dates. Completing an instance leaves the rule unchanged and the completion attached to that date. Editing a rule offers this instance or the whole series and never deletes a completed instance either way. Do not build yet: No export, backup or import yet. ### Phase 5 · A workspace that can leave Build: Export one JSON document holding types, fields including retired ones, relations, entities, values, recurrences and views. Import it into an empty database and compare. Back up the database file on a schedule and test a restore. Trap: Exporting entities and values without the field definitions produces a file full of keys nobody can interpret, and it is only discovered when somebody tries to import it somewhere, by which time the original workspace may be gone. Export the schema in the same document, including retired fields, and prove it by importing into an empty database. Done when: Exporting and importing into an empty database reproduces every type, field, relation, entity, value, recurrence and view, compared programmatically in the suite. Retired fields survive the round trip so their historical values remain interpretable. A restore from backup is exercised in the suite rather than described. The export is a single file that opens in a text editor and is readable without this application. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time multiplayer, which is the feature that makes Fibery a team product and the single largest jump in difficulty available here. - Enterprise permissions, since a workspace with one user has exactly one permission and pretending otherwise adds screens with no behaviour behind them. - An integration catalogue, where each connector is an authentication project plus an ongoing maintenance commitment. - Whiteboards and rich collaborative documents, which are each a substantial editor in their own right. ### README must contain - That types and fields are user-defined and that retiring a field keeps its values readable - The recurrence subset supported, and that invalid dates such as the thirty first of a short month are skipped rather than moved - The measured view query time at a stated workspace size, so a slow filter is a known limit - That export contains the schema and the data in one file and imports into an empty database - Where the database file is, how it is backed up, and that the restore is tested in the suite
# Architecture · Fibery ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript, one process on localhost | one person, one machine, no coordination problem | | Storage | SQLite with a typed entity table plus a per-type field definition table | user-defined types need a schema about schemas, and this is the small version | | Recurrence | A documented subset of the iCalendar recurrence rule, expanded into concrete instances | borrowing a specification beats inventing one and then discovering February | | Views | Saved filters, sorts and groupings stored as data | a view somebody rebuilds every morning is not a view | | Export | One JSON document containing the schema and the data together | data without its schema is a pile of untyped values | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Schema | Types, fields and retirement. | Any store, provided retiring keeps values readable. | | Relations | One link row serving both directions. | Any representation, provided both ends are enforced. | | Views | Filters compiled to queries. | Any query builder, provided filtering never moves into memory. | | Recurrence | Rule expansion following the specification. | Any library implementing the same rule; never a hand-rolled month calculation. | | Export | Schema and data in one document. | Any format, provided it round trips into an empty database. | ## 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 · Everything. One file, backed up on a schedule. - `APP_BIND` · required · Localhost. A single-user workspace has no business on a network interface. - `TIMEZONE` · required · Used for recurrence expansion. Stored per recurrence, not assumed globally. - `RECURRENCE_WINDOW_DAYS` · required · How far ahead instances are materialised. Beyond this they are computed on demand. - `BACKUP_DIR` · required · Scheduled copies of the database file. The restore is tested in the suite. - `BACKUP_KEEP` · required · Retention by count. A workspace file is small and keeping two weeks costs nothing. - `VIEW_QUERY_BUDGET_MS` · required · A view slower than this logs a warning with the compiled query, so it is findable. ## 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 · Fibery product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 with TypeScript, one process on localhost, SQLite with a typed entity table plus a per-type field definition table, A documented subset of the iCalendar recurrence rule, expanded into concrete instances, Saved filters, sorts and groupings stored as data, One JSON document containing the schema and the data together. - 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 - Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. - Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. - A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. - A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. - Exporting entities and values without the field definitions produces a file full of keys nobody can interpret, and it is only discovered when somebody tries to import it somewhere, by which time the original workspace may be gone. Export the schema in the same document, including retired fields, and prove it by importing into an empty database.
# Delivery milestones · Fibery Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Types you can define without breaking yesterday Fields are added, renamed and retired while every existing value keeps its meaning. ### Steps 1. Stable keys, editable labels The same separation that makes a form builder survivable makes a workspace survivable. Files: `src/schema/fields.ts` 2. Retire, never silently delete Hidden and gone look identical in the interface and are not the same thing at all. Files: `src/schema/retire.ts`, `test/retire.test.ts` 3. Validate on write against the kind A typed store that accepts anything is an untyped store with extra tables. Files: `src/values/validate.ts` ### Done when - [ ] Retired fields keep readable values - [ ] Renaming a label changes no value - [ ] Wrong-kind values are rejected by name - [ ] A kind change is refused with the offending values listed ### Watch out - Deleting a field and its values because the user removed it from the form throws away data the user thought they were merely hiding, and they find out three months later when they want it back. Retire the field, hide it from the form, keep the values, and offer a genuine delete as a separate and clearly worded action. ## M2 · Links that survive a deletion Entities link in both directions and a deleted entity never leaves a dangling reference. ### Steps 1. One row, two directions Two rows for one relationship is two chances to disagree. Files: `src/relations/links.ts` 2. Let the database enforce both ends Foreign keys are free and your code is not. Files: `src/db/schema.sql`, `test/dangling.test.ts` 3. Say what a delete will do, first Deleting a project should never quietly orphan forty tasks. Files: `src/components/DeleteDialog.tsx` ### Done when - [ ] Links appear from both sides - [ ] Deletion policy is enforced and shown in advance - [ ] No link can reference a missing entity - [ ] Backlinks list every inbound relation ### Watch out - Storing the relationship twice, once on each side, means the two rows disagree the first time a write fails halfway, and from then on a task appears under a project that does not list it. Store one row, derive both directions from it, and let the database enforce that both ends exist. ## M3 · Saved views that stay fast Filters, sorts and groupings are stored as data and still answer quickly at real size. ### Steps 1. Compile the filter to a query In-memory filtering is fine until the day it is not, and that day arrives silently. Files: `src/views/compile.ts` 2. Seed a realistic workspace for tests Two hundred rows will validate a design that cannot work. Files: `scripts/seed-large.ts` 3. Validate filters when they are saved A broken view should fail in the editor, not in the morning. Files: `src/views/validate.ts` ### Done when - [ ] A three-condition view is measured at realistic size - [ ] Filtering is in the query, not in memory - [ ] Saved views survive a restart identically - [ ] Invalid filters are refused at save time ### Watch out - A value-per-row store makes every multi-condition filter a chain of joins, and a view that is instant over two hundred entities takes seconds over twenty thousand, which is the size a personal workspace reaches in about two years. Compile filters to a single query with indexed lookups, and put a realistic row count into the test data now rather than discovering this later. ## M4 · Recurrence that survives February Repeating work expands correctly on short months, clock changes and edited series. ### Steps 1. Follow the specification, do not clamp An invalid date is skipped, not moved, and this is the whole bug. Files: `src/recurrence/expand.ts`, `test/recurrence.test.ts` 2. Expand over a bounded window An infinite series materialised into rows is a full disk with a to-do list on it. Files: `src/recurrence/window.ts` 3. Keep completions attached to dates Editing a series should never erase what somebody already did. Files: `src/recurrence/instances.ts` ### Done when - [ ] A monthly rule on the thirty first skips short months - [ ] A daily rule fires once per local day across a clock change - [ ] Completing an instance leaves the rule unchanged - [ ] Editing a series preserves completed instances ### Watch out - A monthly task on the thirty first is where every homemade recurrence engine goes wrong: clamping to the last day of a short month invents an occurrence on the thirtieth that the rule never described, and if the series is bounded by a count it also moves where the series ends. The iCalendar specification is unambiguous that instances with an invalid date must be ignored and must not be counted as part of the recurrence set, and it also forbids a rule carrying both a count and an end date. Follow the specification and test the thirty first of every month. ## M5 · A workspace that can leave (production only) Export carries the schema with the data and importing it reproduces the workspace exactly. ### Steps 1. Export the schema with the data Values without their field definitions are a puzzle, not a backup. Files: `src/export/workspace.ts` 2. Round trip into an empty database in the suite The only export test that means anything. Files: `test/roundtrip.test.ts` 3. Test the restore, not just the backup A backup nobody has restored is a file of unknown quality. Files: `scripts/backup.sh`, `test/restore.test.ts` ### Done when - [ ] A round trip reproduces the workspace exactly - [ ] Retired fields survive the export - [ ] Restore is exercised in the suite - [ ] The export file is readable without the app ### Watch out - Exporting entities and values without the field definitions produces a file full of keys nobody can interpret, and it is only discovered when somebody tries to import it somewhere, by which time the original workspace may be gone. Export the schema in the same document, including retired fields, and prove it by importing into an empty database.
# Operations · Fibery ## Backup The single database file on a schedule, with retention by count. It is small enough that keeping a fortnight costs nothing and losing a fortnight costs a workspace. ## Restore Restore into an empty directory, open the application, and check the entity count and a saved view against what you expect. The round trip test covers the format; this drill covers the procedure. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Views exceeding the query budget, the count of materialised recurrence instances, and backup age. A growing instance count usually means a rule with no end that nobody noticed writing rows. ## Incident checklist A missing task is nearly always a view filter rather than lost data, and the fastest check is querying the entity directly. A wrong recurrence date is a specification question, and the suite already contains the answer for short months and clock changes. 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 - [ ] Retired fields are confirmed to keep readable values - [ ] A link is confirmed unable to reference a missing entity - [ ] A three-condition view is confirmed measured at realistic size - [ ] A monthly rule on the thirty first is confirmed to skip short months - [ ] An export is confirmed to round trip into an empty database ## Launch constraint Do not market omitted Fibery 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. Everything. One file, backed up on a schedule. DATABASE_PATH=./data/workspace.db # Required. Localhost. A single-user workspace has no business on a network interface. APP_BIND=127.0.0.1:3000 # Required. Used for recurrence expansion. Stored per recurrence, not assumed globally. TIMEZONE=Europe/London # Required. How far ahead instances are materialised. Beyond this they are computed on demand. RECURRENCE_WINDOW_DAYS=180 # Required. Scheduled copies of the database file. The restore is tested in the suite. BACKUP_DIR=./data/backups # Required. Retention by count. A workspace file is small and keeping two weeks costs nothing. BACKUP_KEEP=14 # Required. A view slower than this logs a warning with the compiled query, so it is findable. VIEW_QUERY_BUDGET_MS=200
$ choose a build depth, inspect the files, then open the complete pack in your agent
Fibery: 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
Fibery pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | Up to 10 users + 10 guests; 10 databases; 1 GB storage; 14-day history; 250 automation runs/month; 5,000 syncs/month; 1 integration; guests can access up to 10 shared entities. |
| standard | $17.33/user | $13.86/user | 250 AI credits/seat/month; paid automation and sync quotas scale with seats. |
| pro | $28.88/user | $23.11/user | 500 AI credits/seat/month; expanded history, storage, automations, and integrations. |
| enterprise | $57.77/user | $46.21/user | Minimum 25 paid users; 1,000 AI credits/seat/month; enterprise controls. |
free tierUp to 10 users + 10 guests; 10 databases; 1 GB storage; 14-day history; 250 automation runs/month; 5,000 syncs/month; 1 integration; guests can access up to 10 shared entities.
billingmonthly + annual (-20%)
hidden costsEnterprise has a 25-paid-user minimum; migration assistance may be separately quoted; paid users are prorated while observers/guests can be free within plan rules; VAT may apply.
verified 2026-08-14 · source ↗
Vibecode Fibery
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Fibery replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Fibery cost?
Fibery'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 Fibery?
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 Fibery?
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.