Build 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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the application and the seed scripts.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree
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 - have readyfree
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 - have readyfree
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 - decidefree
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
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- 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
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/workspace.db | Everything. One file, backed up on a schedule. |
APP_BIND | required | 127.0.0.1:3000 | Localhost. A single-user workspace has no business on a network interface. |
TIMEZONE | required | Europe/London | Used for recurrence expansion. Stored per recurrence, not assumed globally. |
RECURRENCE_WINDOW_DAYS | required | 180 | How far ahead instances are materialised. Beyond this they are computed on demand. |
BACKUP_DIR | required | ./data/backups | Scheduled copies of the database file. The restore is tested in the suite. |
BACKUP_KEEP | required | 14 | Retention by count. A workspace file is small and keeping two weeks costs nothing. |
VIEW_QUERY_BUDGET_MS | required | 200 | A view slower than this logs a warning with the compiled query, so it is findable. |
The build, in order
Types you can define without breaking yesterday
Fields are added, renamed and retired while every existing value keeps its meaning.
The same separation that makes a form builder survivable makes a workspace survivable.
Files
src/schema/fields.tsHidden and gone look identical in the interface and are not the same thing at all.
Files
src/schema/retire.tstest/retire.test.tsA typed store that accepts anything is an untyped store with extra tables.
Files
src/values/validate.ts
done when · tick each as it passeswatch 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.
Links that survive a deletion
Entities link in both directions and a deleted entity never leaves a dangling reference.
Two rows for one relationship is two chances to disagree.
Files
src/relations/links.tsForeign keys are free and your code is not.
Files
src/db/schema.sqltest/dangling.test.tsDeleting a project should never quietly orphan forty tasks.
Files
src/components/DeleteDialog.tsx
done when · tick each as it passeswatch 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.
Saved views that stay fast
Filters, sorts and groupings are stored as data and still answer quickly at real size.
In-memory filtering is fine until the day it is not, and that day arrives silently.
Files
src/views/compile.tsTwo hundred rows will validate a design that cannot work.
Files
scripts/seed-large.tsA broken view should fail in the editor, not in the morning.
Files
src/views/validate.ts
done when · tick each as it passeswatch 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.
Recurrence that survives February
Repeating work expands correctly on short months, clock changes and edited series.
An invalid date is skipped, not moved, and this is the whole bug.
Files
src/recurrence/expand.tstest/recurrence.test.tsAn infinite series materialised into rows is a full disk with a to-do list on it.
Files
src/recurrence/window.tsEditing a series should never erase what somebody already did.
Files
src/recurrence/instances.ts
done when · tick each as it passeswatch 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.
A workspace that can leaveproduct builder
Export carries the schema with the data and importing it reproduces the workspace exactly.
Values without their field definitions are a puzzle, not a backup.
Files
src/export/workspace.tsThe only export test that means anything.
Files
test/roundtrip.test.tsA backup nobody has restored is a file of unknown quality.
Files
scripts/backup.shtest/restore.test.ts
done when · tick each as it passeswatch 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.
That is the whole plan for Fibery. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- 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
- 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
Need the files? The project pack on the verdict page hands your agent the whole brief · more tasks.