Build Coda
KINDAreplaces $12/mosaves $144/yrback to the verdict
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.
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 server and the build.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - 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 - 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://user:pass@localhost:5432/docs | Your PostgreSQL connection string. |
BUTTON_CONFIRM_ROWS | required | 25 | Above this many affected rows, a button press requires confirmation. |
MAX_FORMULA_DEPTH | required | 20 | Maximum dependency chain length. A cap that catches pathological documents before they hang. |
RECOMPUTE_BUDGET_MS | required | 500 | Soft budget for a recomputation pass. Exceeding it is logged, since it means the graph has grown. |
EXPORT_DIR | required | ./exports | Where document exports and their CSV tables are written. |
The build, in order
Documents with real tables in them
A table inside a document is data, and editing a cell in one place shows everywhere.
A block references a table. The table is the data and lives once.
Files
src/model.tsNumber, date, select and reference all reject invalid input at the edge rather than storing it and failing later.
Files
src/columns.tsDeleting a document must not orphan a table that another document uses.
Files
src/tables.ts
done when · tick each as it passeswatch 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.
A formula language you wrote down first
Formulas evaluate correctly, and the language is exactly as large as you specified.
In a file, before any parser code. Literals, references, operators and a closed function list.
Files
docs/formula-grammar.mdAnything not in the grammar is a parse error, not a special case.
Files
src/formula/parse.tssrc/formula/eval.tsKnown 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 passeswatch 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.
Recomputing only what changed
Editing one cell in a large table is instant.
Updated when a formula changes, not recomputed by scanning on every edit.
Files
src/deps.tsOnly dependents of the changed cell, in order. Count evaluations in a test.
Files
src/recompute.tsRefuse 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 passeswatch 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.
Views over one table
The same data appears filtered and grouped in several places without ever being copied.
Filter, group and sort computed at read time against the table.
Files
src/views.tsA view is a lens. Editing through it edits the table, with no intermediate copy.
Files
src/views.tsThey 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 passeswatch 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.
Buttons that do a fixed set of things
A button in a document performs a defined action, and its blast radius is known.
Three kinds, configured through a form. No scripting surface anywhere.
Files
src/buttons.tsWho, when, what changed. A button that altered forty rows must be traceable.
Files
src/audit.tsThe protection against a misconfigured filter plus a set-value action.
Files
src/buttons.ts
done when · tick each as it passeswatch 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.
Sharing, permissions and exportproduct builder
Someone else can open a document without being able to break it, and nothing is trapped.
In every handler. Test with a direct request rather than through the interface.
Files
src/auth.tsMarkdown for prose, CSV for tables, and the formula definitions alongside so nothing is lost.
Files
src/export.tsRe-import and compare. An export you cannot re-import is a screenshot.
Files
src/export.test.ts
done when · tick each as it passeswatch out- Enforcing read-only in the interface leaves the write endpoints open to anyone who can reach them. Every permission check belongs in the handler, and the test that matters is a direct request with a viewer's credentials.
That is the whole plan for Coda. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A general purpose formula language.
- The pack and integration ecosystem.
- Real-time collaborative editing.
- An organisational permission model.
- formula language
- packs
- permissions
- automations
- templates
- collaboration
- doc-app UX
- Cross-table references, which need the dependency graph to span tables
- Scheduled button actions, reusing the closed action set
- A formula debugger showing the evaluation tree for one cell
Need the files? The project pack on the verdict page hands your agent the whole brief · more docs & databases.