Build Coda

KINDAreplaces $12/mosaves $144/yrback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A document editor with embedded tables that are real data rather than static grids, a small explicit formula language over columns and rows with a dependency graph that recomputes only what changed, buttons that run defined actions, and views that filter and group the same underlying table.

estimated effort multi-daythe files for this build are in the project pack

RuntimeTypeScript with a small server and PostgreSQLEditorA structured document model with typed blocksFormulasA hand-written parser and evaluator over a written grammarRecomputeA dependency graph with topological evaluationViewsFilters and grouping over one table, never copies

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the server and the build.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree

    Why Holds documents, tables and rows. A document store would make querying views painful.

    Get it Install locally or run it in Docker. open ↗

    Verify psql connects and reports 16 or higher

  3. decidefree

    Why A general document tool is unbuildable and a specific one is a weekend. The formula language should be exactly large enough for this document and no larger.

    Get it Pick a real thing: a project tracker with rollups, a reading list with computed status, an invoice log with totals. Write down which formulas it needs before Phase 2.

    Verify The required function list is written down and is short

  4. decidefree

    Why It is the difference between a formula language you can implement correctly and one that grows by accretion into something unpredictable.

    Get it Half a page describing literals, references, operators, precedence and the closed function list. Written before Phase 2 begins.

    Verify The grammar file exists and predates the parser

  5. have readyfree

    Why Phase 3's dependency graph is only justified against a table large enough that naive recomputation is visibly wrong.

    Get it Generate it with formula columns depending on each other.

    Verify The fixture loads and full recompute time is measurable

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- docs(id, title, created_at), blocks(id, doc_id, parent_id, ordinal, kind, content_json) · kind is text, heading, table_ref, view or button
- tables(id, doc_id, name), columns(id, table_id, name, kind, ordinal, formula) · kind is text, number, date, select, reference or formula
- rows(id, table_id, ordinal), cells(row_id, column_id, value_json, computed_value_json, computed_at)
- views(id, block_id, table_id, filter_json, group_by_column_id, sort_json)
- deps(from_column_id, to_column_id) · the dependency graph, maintained as formulas change
- buttons(id, block_id, label, action_kind, params_json) · action_kind is add_row, set_value or run_webhook
- A view never copies rows. It is a query over the table, which is what keeps two places from disagreeing.

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.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://user:pass@localhost:5432/docsYour PostgreSQL connection string.
BUTTON_CONFIRM_ROWSrequired25Above this many affected rows, a button press requires confirmation.
MAX_FORMULA_DEPTHrequired20Maximum dependency chain length. A cap that catches pathological documents before they hang.
RECOMPUTE_BUDGET_MSrequired500Soft budget for a recomputation pass. Exceeding it is logged, since it means the graph has grown.
EXPORT_DIRrequired./exportsWhere document exports and their CSV tables are written.

The build, in order

  1. Documents with real tables in them

    A table inside a document is data, and editing a cell in one place shows everywhere.

    1. A block references a table. The table is the data and lives once.

      Files src/model.ts

    2. Number, date, select and reference all reject invalid input at the edge rather than storing it and failing later.

      Files src/columns.ts

    3. Deleting a document must not orphan a table that another document uses.

      Files src/tables.ts

    done when · tick each as it passes
    watch out
    • Storing table data inside the document block makes a second reference to the same table a copy, and copies diverge within a day. Reference the table by identifier from the block, so a document contains a pointer and the data has one home.
  2. A formula language you wrote down first

    Formulas evaluate correctly, and the language is exactly as large as you specified.

    1. In a file, before any parser code. Literals, references, operators and a closed function list.

      Files docs/formula-grammar.md

    2. Anything not in the grammar is a parse error, not a special case.

      Files src/formula/parse.tssrc/formula/eval.ts

    3. Known inputs and known results. A function without a test is a function you will get wrong.

      Files src/formula/eval.test.ts

    done when · tick each as it passes
    watch out
    • Growing a formula language by adding a case whenever something does not work produces a language nobody can predict, including you, and errors that are impossible to explain. Write the grammar first, keep the function list closed, and treat every addition as a deliberate language change.
  3. Recomputing only what changed

    Editing one cell in a large table is instant.

    1. Updated when a formula changes, not recomputed by scanning on every edit.

      Files src/deps.ts

    2. Only dependents of the changed cell, in order. Count evaluations in a test.

      Files src/recompute.ts

    3. Refuse the formula and name the cycle. Discovering it at evaluation time is too late and produces a hang.

      Files src/deps.ts

    done when · tick each as it passes
    watch out
    • Recomputing the whole table on every edit is fine at fifty rows and takes seconds at five thousand, and by then the architecture is baked in. Build the dependency graph in this phase, against a table with thousands of rows, rather than after users complain.
  4. Views over one table

    The same data appears filtered and grouped in several places without ever being copied.

    1. Filter, group and sort computed at read time against the table.

      Files src/views.ts

    2. A view is a lens. Editing through it edits the table, with no intermediate copy.

      Files src/views.ts

    3. They leave the view and stay in the table. Test this explicitly; it is the case that reveals a materialised view.

      Files src/views.test.ts

    done when · tick each as it passes
    watch out
    • Materialising a view as its own rows makes editing ambiguous and the two copies diverge the moment anything changes. A view is a query. If it feels like it needs its own rows, what you actually want is a second table.
  5. Buttons that do a fixed set of things

    A button in a document performs a defined action, and its blast radius is known.

    1. Three kinds, configured through a form. No scripting surface anywhere.

      Files src/buttons.ts

    2. Who, when, what changed. A button that altered forty rows must be traceable.

      Files src/audit.ts

    3. The protection against a misconfigured filter plus a set-value action.

      Files src/buttons.ts

    done when · tick each as it passes
    watch out
    • Letting a button run arbitrary script turns a document into a program, and a shared document into a program written by someone else running on your data. Keep the action set closed and configured, because that is what makes a shared document safe to open.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more docs & databases.