A doc with tables and buttons is buildable; Coda's editor, formula engine, packs/integrations, permissions, and team workflows make full parity difficult.
You are building a lean indie version of Coda. 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 ===== # Coda · indie build 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-day**. 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 | TypeScript with a small server and PostgreSQL | tables are data and belong in a database rather than in a document blob | | Editor | A structured document model with typed blocks | a document that embeds live tables cannot be a string of Markdown | | Formulas | A hand-written parser and evaluator over a written grammar | a grammar you wrote down is a grammar you can implement correctly; one you grew by accretion is not | | Recompute | A dependency graph with topological evaluation | recomputing every formula on every edit is fine at ten rows and unusable at a thousand | | Views | Filters and grouping over one table, never copies | two copies of the same data diverge, and reconciling them is the bug you will spend a month on | ## 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 server and the build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free - Why: Holds documents, tables and rows. A document store would make querying views painful. - Get it: Install locally or run it in Docker. - Verify: psql connects and reports 16 or higher - [ ] **One real document you actually need** · free - 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 - [ ] **Willingness to write a grammar before code** · free - 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 - [ ] **A generated table of five thousand rows** · free - 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 ## 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: - 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 If one of those is essential to you, that is the reason to keep paying for Coda, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Coda 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 document that contains real tables, a formula language and buttons that do things to replace Coda. 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. ### Rule zero The temptation here is to build a general purpose formula language, and that is where this project dies. A formula language is a parser, an evaluator, a dependency graph, a cycle detector and an error model, and every one of them has to be right before a single formula is trustworthy. So build the smallest formula language that does your actual work, define its grammar in writing before you implement it, and refuse to grow it by adding special cases. If you need general computation, the honest answer is a spreadsheet next to a document rather than one tool pretending to be both. ### Stack (fixed, do not substitute) - Runtime: TypeScript with a small server and PostgreSQL · tables are data and belong in a database rather than in a document blob - Editor: A structured document model with typed blocks · a document that embeds live tables cannot be a string of Markdown - Formulas: A hand-written parser and evaluator over a written grammar · a grammar you wrote down is a grammar you can implement correctly; one you grew by accretion is not - Recompute: A dependency graph with topological evaluation · recomputing every formula on every edit is fine at ten rows and unusable at a thousand - Views: Filters and grouping over one table, never copies · two copies of the same data diverge, and reconciling them is the bug you will spend a month on ### Data model (create this before Phase 1) - 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. ### Phase 1 · Documents with real tables in them Build: A block-based document model with typed blocks. A table block referencing a table rather than containing it. Typed columns with validation. Two blocks referencing the same table show the same data. Trap: 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. Done when: Two table blocks in different documents referencing one table show the same rows, and editing through either updates both. A number column rejects text with a visible error rather than storing it. Reordering rows persists. Deleting a document does not delete a table another document references. Do not build yet: No formulas, no views, no buttons. ### Phase 2 · A formula language you wrote down first Build: Write the grammar before implementing: literals, column references, arithmetic, comparison, and a fixed list of functions. Then write the parser and evaluator against it. Reject anything outside the grammar with a clear error rather than guessing. Trap: 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. Done when: The written grammar exists before the parser and the implementation matches it. Every function in the list has a test with a known result. A formula outside the grammar produces a clear parse error naming the position. A formula referencing a missing column produces a named error rather than a null. Do not build yet: No recomputation strategy yet, so evaluation may be naive. ### Phase 3 · Recomputing only what changed Build: Build a dependency graph from formulas. On a cell change, recompute only the dependents in topological order. Detect cycles when a formula is saved and refuse it with the cycle named. Test with a table large enough to make a naive approach obviously wrong. Trap: 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. Done when: Editing one cell in a five thousand row table recomputes only its dependents, verified by counting evaluations. A formula creating a cycle is refused at save time with the cycle named. Recomputation order is correct for a chain of three dependent formula columns. A full recompute of the large table completes in a stated time. Do not build yet: No views or buttons yet. ### Phase 4 · Views over one table Build: Views with filters, grouping and sorting, evaluated as queries against the table. Editing a row through a view edits the table. A row that no longer matches a filter leaves the view rather than becoming a stale copy. Trap: 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. Done when: Editing a row through a filtered view updates the underlying table. A row edited so it no longer matches the filter disappears from that view and remains in the table. Grouping shows correct counts per group. Two views of one table with different filters both reflect an edit made in either. Do not build yet: No buttons or automations yet. ### Phase 5 · Buttons that do a fixed set of things Build: A closed set of button actions: add a row, set a value, and call a webhook. Each is configured rather than scripted. Log every press with what it did. Confirm before an action that affects more than a threshold of rows. Trap: 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. Done when: Each action kind works and is logged with what it changed. An action affecting more rows than the threshold requires confirmation. There is no code path that executes user-supplied script, asserted by a test. A webhook action's failure is visible rather than silent. Do not build yet: No sharing or permissions yet. ### Phase 6 · Sharing, permissions and export Build: Per-document permissions of view, comment and edit, enforced on the server. Export a document as Markdown with tables as CSV alongside. Import the same back. Then confirm the export is usable without this application. Trap: 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. Done when: A viewer cannot write, tested by calling the write endpoint directly with viewer credentials. A document exports as Markdown with tables as CSV files. The export re-imports and produces the same tables and formulas. A shared document's buttons respect the viewer's permission. Do not build yet: Nothing after this. ### Out of scope (and why) - A general purpose formula language. The commercial product's language is large, well documented and consistent, and matching it is a multi-year project that rule zero exists to prevent you starting. - The pack ecosystem that connects documents to other services, which is other people's integration work. - Real-time collaborative editing of documents and tables together, which is meaningfully harder than either alone. - The permission model a company needs, with groups, inheritance and sharing policy. ### README must contain - That the formula grammar is written down and closed, and where the file is - That tables live once and blocks reference them, so a view is never a copy - That recomputation walks a dependency graph rather than re-evaluating the table - That button actions are a closed configured set with no scripting surface - That permissions are enforced in handlers rather than in the interface ===== AGENTS.md ===== # Agent instructions · Coda indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with a small server and PostgreSQL, A structured document model with typed blocks, A hand-written parser and evaluator over a written grammar, A dependency graph with topological evaluation, Filters and grouping over one table, never copies. 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 - 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. - 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 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Coda 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Documents with real tables in them A table inside a document is data, and editing a cell in one place shows everywhere. ### Steps 1. Model blocks and tables separately A block references a table. The table is the data and lives once. Files: `src/model.ts` 2. Type and validate columns Number, date, select and reference all reject invalid input at the edge rather than storing it and failing later. Files: `src/columns.ts` 3. Handle shared references Deleting a document must not orphan a table that another document uses. Files: `src/tables.ts` ### Done when - [ ] Two blocks referencing one table stay in sync - [ ] A number column rejects text with a visible error - [ ] Row order persists - [ ] Deleting a document does not delete a referenced table ### 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. ## Phase 2 · A formula language you wrote down first Formulas evaluate correctly, and the language is exactly as large as you specified. ### Steps 1. Write the grammar In a file, before any parser code. Literals, references, operators and a closed function list. Files: `docs/formula-grammar.md` 2. Implement parser and evaluator against it Anything not in the grammar is a parse error, not a special case. Files: `src/formula/parse.ts`, `src/formula/eval.ts` 3. Test every function 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 - [ ] The grammar file predates the parser and matches it - [ ] Every function has a test with a known result - [ ] An out-of-grammar formula gives a positioned parse error - [ ] A missing column reference gives a named error, not a null ### 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. ## Phase 3 · Recomputing only what changed Editing one cell in a large table is instant. ### Steps 1. Maintain the dependency graph Updated when a formula changes, not recomputed by scanning on every edit. Files: `src/deps.ts` 2. Evaluate topologically Only dependents of the changed cell, in order. Count evaluations in a test. Files: `src/recompute.ts` 3. Detect cycles at save 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 - [ ] One cell edit recomputes only dependents, proved by evaluation count - [ ] A cyclic formula is refused at save with the cycle named - [ ] A three-deep formula chain evaluates in the right order - [ ] Full recompute of a five thousand row table completes in a stated time ### 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. ## Phase 4 · Views over one table The same data appears filtered and grouped in several places without ever being copied. ### Steps 1. Evaluate views as queries Filter, group and sort computed at read time against the table. Files: `src/views.ts` 2. Make edits write through A view is a lens. Editing through it edits the table, with no intermediate copy. Files: `src/views.ts` 3. Handle rows leaving a view 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 - [ ] Editing through a view updates the table - [ ] A row that stops matching leaves the view and stays in the table - [ ] Group counts are correct - [ ] Two views reflect an edit made in either ### 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. ## Phase 5 · Buttons that do a fixed set of things A button in a document performs a defined action, and its blast radius is known. ### Steps 1. Implement the closed action set Three kinds, configured through a form. No scripting surface anywhere. Files: `src/buttons.ts` 2. Log every press Who, when, what changed. A button that altered forty rows must be traceable. Files: `src/audit.ts` 3. Confirm above a threshold The protection against a misconfigured filter plus a set-value action. Files: `src/buttons.ts` ### Done when - [ ] Each action kind works and is logged - [ ] A large action requires confirmation - [ ] No code path executes user script, asserted by test - [ ] A failed webhook is visible ### 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. ## Not in this build - A general purpose formula language. - The pack and integration ecosystem. - Real-time collaborative editing. - An organisational permission model. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/docs # Required. Above this many affected rows, a button press requires confirmation. BUTTON_CONFIRM_ROWS=25 # Required. Maximum dependency chain length. A cap that catches pathological documents before they hang. MAX_FORMULA_DEPTH=20 # Required. Soft budget for a recomputation pass. Exceeding it is logged, since it means the graph has grown. RECOMPUTE_BUDGET_MS=500 # Required. Where document exports and their CSV tables are written. EXPORT_DIR=./exports
You are building a lean indie version of Coda. 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 ===== # Coda · indie build 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-day**. 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 | TypeScript with a small server and PostgreSQL | tables are data and belong in a database rather than in a document blob | | Editor | A structured document model with typed blocks | a document that embeds live tables cannot be a string of Markdown | | Formulas | A hand-written parser and evaluator over a written grammar | a grammar you wrote down is a grammar you can implement correctly; one you grew by accretion is not | | Recompute | A dependency graph with topological evaluation | recomputing every formula on every edit is fine at ten rows and unusable at a thousand | | Views | Filters and grouping over one table, never copies | two copies of the same data diverge, and reconciling them is the bug you will spend a month on | ## 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 server and the build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free - Why: Holds documents, tables and rows. A document store would make querying views painful. - Get it: Install locally or run it in Docker. - Verify: psql connects and reports 16 or higher - [ ] **One real document you actually need** · free - 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 - [ ] **Willingness to write a grammar before code** · free - 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 - [ ] **A generated table of five thousand rows** · free - 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 ## 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: - 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 If one of those is essential to you, that is the reason to keep paying for Coda, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Coda 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 document that contains real tables, a formula language and buttons that do things to replace Coda. 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. ### Rule zero The temptation here is to build a general purpose formula language, and that is where this project dies. A formula language is a parser, an evaluator, a dependency graph, a cycle detector and an error model, and every one of them has to be right before a single formula is trustworthy. So build the smallest formula language that does your actual work, define its grammar in writing before you implement it, and refuse to grow it by adding special cases. If you need general computation, the honest answer is a spreadsheet next to a document rather than one tool pretending to be both. ### Stack (fixed, do not substitute) - Runtime: TypeScript with a small server and PostgreSQL · tables are data and belong in a database rather than in a document blob - Editor: A structured document model with typed blocks · a document that embeds live tables cannot be a string of Markdown - Formulas: A hand-written parser and evaluator over a written grammar · a grammar you wrote down is a grammar you can implement correctly; one you grew by accretion is not - Recompute: A dependency graph with topological evaluation · recomputing every formula on every edit is fine at ten rows and unusable at a thousand - Views: Filters and grouping over one table, never copies · two copies of the same data diverge, and reconciling them is the bug you will spend a month on ### Data model (create this before Phase 1) - 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. ### Phase 1 · Documents with real tables in them Build: A block-based document model with typed blocks. A table block referencing a table rather than containing it. Typed columns with validation. Two blocks referencing the same table show the same data. Trap: 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. Done when: Two table blocks in different documents referencing one table show the same rows, and editing through either updates both. A number column rejects text with a visible error rather than storing it. Reordering rows persists. Deleting a document does not delete a table another document references. Do not build yet: No formulas, no views, no buttons. ### Phase 2 · A formula language you wrote down first Build: Write the grammar before implementing: literals, column references, arithmetic, comparison, and a fixed list of functions. Then write the parser and evaluator against it. Reject anything outside the grammar with a clear error rather than guessing. Trap: 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. Done when: The written grammar exists before the parser and the implementation matches it. Every function in the list has a test with a known result. A formula outside the grammar produces a clear parse error naming the position. A formula referencing a missing column produces a named error rather than a null. Do not build yet: No recomputation strategy yet, so evaluation may be naive. ### Phase 3 · Recomputing only what changed Build: Build a dependency graph from formulas. On a cell change, recompute only the dependents in topological order. Detect cycles when a formula is saved and refuse it with the cycle named. Test with a table large enough to make a naive approach obviously wrong. Trap: 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. Done when: Editing one cell in a five thousand row table recomputes only its dependents, verified by counting evaluations. A formula creating a cycle is refused at save time with the cycle named. Recomputation order is correct for a chain of three dependent formula columns. A full recompute of the large table completes in a stated time. Do not build yet: No views or buttons yet. ### Phase 4 · Views over one table Build: Views with filters, grouping and sorting, evaluated as queries against the table. Editing a row through a view edits the table. A row that no longer matches a filter leaves the view rather than becoming a stale copy. Trap: 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. Done when: Editing a row through a filtered view updates the underlying table. A row edited so it no longer matches the filter disappears from that view and remains in the table. Grouping shows correct counts per group. Two views of one table with different filters both reflect an edit made in either. Do not build yet: No buttons or automations yet. ### Phase 5 · Buttons that do a fixed set of things Build: A closed set of button actions: add a row, set a value, and call a webhook. Each is configured rather than scripted. Log every press with what it did. Confirm before an action that affects more than a threshold of rows. Trap: 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. Done when: Each action kind works and is logged with what it changed. An action affecting more rows than the threshold requires confirmation. There is no code path that executes user-supplied script, asserted by a test. A webhook action's failure is visible rather than silent. Do not build yet: No sharing or permissions yet. ### Phase 6 · Sharing, permissions and export Build: Per-document permissions of view, comment and edit, enforced on the server. Export a document as Markdown with tables as CSV alongside. Import the same back. Then confirm the export is usable without this application. Trap: 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. Done when: A viewer cannot write, tested by calling the write endpoint directly with viewer credentials. A document exports as Markdown with tables as CSV files. The export re-imports and produces the same tables and formulas. A shared document's buttons respect the viewer's permission. Do not build yet: Nothing after this. ### Out of scope (and why) - A general purpose formula language. The commercial product's language is large, well documented and consistent, and matching it is a multi-year project that rule zero exists to prevent you starting. - The pack ecosystem that connects documents to other services, which is other people's integration work. - Real-time collaborative editing of documents and tables together, which is meaningfully harder than either alone. - The permission model a company needs, with groups, inheritance and sharing policy. ### README must contain - That the formula grammar is written down and closed, and where the file is - That tables live once and blocks reference them, so a view is never a copy - That recomputation walks a dependency graph rather than re-evaluating the table - That button actions are a closed configured set with no scripting surface - That permissions are enforced in handlers rather than in the interface ===== AGENTS.md ===== # Agent instructions · Coda indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with a small server and PostgreSQL, A structured document model with typed blocks, A hand-written parser and evaluator over a written grammar, A dependency graph with topological evaluation, Filters and grouping over one table, never copies. 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 - 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. - 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 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Coda 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Documents with real tables in them A table inside a document is data, and editing a cell in one place shows everywhere. ### Steps 1. Model blocks and tables separately A block references a table. The table is the data and lives once. Files: `src/model.ts` 2. Type and validate columns Number, date, select and reference all reject invalid input at the edge rather than storing it and failing later. Files: `src/columns.ts` 3. Handle shared references Deleting a document must not orphan a table that another document uses. Files: `src/tables.ts` ### Done when - [ ] Two blocks referencing one table stay in sync - [ ] A number column rejects text with a visible error - [ ] Row order persists - [ ] Deleting a document does not delete a referenced table ### 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. ## Phase 2 · A formula language you wrote down first Formulas evaluate correctly, and the language is exactly as large as you specified. ### Steps 1. Write the grammar In a file, before any parser code. Literals, references, operators and a closed function list. Files: `docs/formula-grammar.md` 2. Implement parser and evaluator against it Anything not in the grammar is a parse error, not a special case. Files: `src/formula/parse.ts`, `src/formula/eval.ts` 3. Test every function 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 - [ ] The grammar file predates the parser and matches it - [ ] Every function has a test with a known result - [ ] An out-of-grammar formula gives a positioned parse error - [ ] A missing column reference gives a named error, not a null ### 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. ## Phase 3 · Recomputing only what changed Editing one cell in a large table is instant. ### Steps 1. Maintain the dependency graph Updated when a formula changes, not recomputed by scanning on every edit. Files: `src/deps.ts` 2. Evaluate topologically Only dependents of the changed cell, in order. Count evaluations in a test. Files: `src/recompute.ts` 3. Detect cycles at save 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 - [ ] One cell edit recomputes only dependents, proved by evaluation count - [ ] A cyclic formula is refused at save with the cycle named - [ ] A three-deep formula chain evaluates in the right order - [ ] Full recompute of a five thousand row table completes in a stated time ### 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. ## Phase 4 · Views over one table The same data appears filtered and grouped in several places without ever being copied. ### Steps 1. Evaluate views as queries Filter, group and sort computed at read time against the table. Files: `src/views.ts` 2. Make edits write through A view is a lens. Editing through it edits the table, with no intermediate copy. Files: `src/views.ts` 3. Handle rows leaving a view 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 - [ ] Editing through a view updates the table - [ ] A row that stops matching leaves the view and stays in the table - [ ] Group counts are correct - [ ] Two views reflect an edit made in either ### 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. ## Phase 5 · Buttons that do a fixed set of things A button in a document performs a defined action, and its blast radius is known. ### Steps 1. Implement the closed action set Three kinds, configured through a form. No scripting surface anywhere. Files: `src/buttons.ts` 2. Log every press Who, when, what changed. A button that altered forty rows must be traceable. Files: `src/audit.ts` 3. Confirm above a threshold The protection against a misconfigured filter plus a set-value action. Files: `src/buttons.ts` ### Done when - [ ] Each action kind works and is logged - [ ] A large action requires confirmation - [ ] No code path executes user script, asserted by test - [ ] A failed webhook is visible ### 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. ## Not in this build - A general purpose formula language. - The pack and integration ecosystem. - Real-time collaborative editing. - An organisational permission model. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/docs # Required. Above this many affected rows, a button press requires confirmation. BUTTON_CONFIRM_ROWS=25 # Required. Maximum dependency chain length. A cap that catches pathological documents before they hang. MAX_FORMULA_DEPTH=20 # Required. Soft budget for a recomputation pass. Exceeding it is logged, since it means the graph has grown. RECOMPUTE_BUDGET_MS=500 # Required. Where document exports and their CSV tables are written. EXPORT_DIR=./exports
You are building a production product version of Coda. 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 ===== # Coda · product brief ## Problem A doc with tables and buttons is buildable; Coda's editor, formula engine, packs/integrations, permissions, and team workflows make full parity difficult. ## Product outcome A document tool whose formula language is small and specified, whose tables have one home, whose recomputation is proportional to what changed, and whose buttons cannot run arbitrary code. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - rich text editor - database tables - formula engine - auth - integrations - hosted backend ## Explicit non-goals for v1 - 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 ## Success criteria - The grammar file matches the implemented parser - Every formula function has a passing test - One cell edit is confirmed to recompute only dependents - No user script execution path exists, asserted by test - A viewer's direct write request is confirmed refused ===== BRIEF.md ===== # Build brief · Coda 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 document that contains real tables, a formula language and buttons that do things to replace Coda. 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. ### Rule zero The temptation here is to build a general purpose formula language, and that is where this project dies. A formula language is a parser, an evaluator, a dependency graph, a cycle detector and an error model, and every one of them has to be right before a single formula is trustworthy. So build the smallest formula language that does your actual work, define its grammar in writing before you implement it, and refuse to grow it by adding special cases. If you need general computation, the honest answer is a spreadsheet next to a document rather than one tool pretending to be both. ### Stack (fixed, do not substitute) - Runtime: TypeScript with a small server and PostgreSQL · tables are data and belong in a database rather than in a document blob - Editor: A structured document model with typed blocks · a document that embeds live tables cannot be a string of Markdown - Formulas: A hand-written parser and evaluator over a written grammar · a grammar you wrote down is a grammar you can implement correctly; one you grew by accretion is not - Recompute: A dependency graph with topological evaluation · recomputing every formula on every edit is fine at ten rows and unusable at a thousand - Views: Filters and grouping over one table, never copies · two copies of the same data diverge, and reconciling them is the bug you will spend a month on ### Data model (create this before Phase 1) - 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. ### Phase 1 · Documents with real tables in them Build: A block-based document model with typed blocks. A table block referencing a table rather than containing it. Typed columns with validation. Two blocks referencing the same table show the same data. Trap: 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. Done when: Two table blocks in different documents referencing one table show the same rows, and editing through either updates both. A number column rejects text with a visible error rather than storing it. Reordering rows persists. Deleting a document does not delete a table another document references. Do not build yet: No formulas, no views, no buttons. ### Phase 2 · A formula language you wrote down first Build: Write the grammar before implementing: literals, column references, arithmetic, comparison, and a fixed list of functions. Then write the parser and evaluator against it. Reject anything outside the grammar with a clear error rather than guessing. Trap: 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. Done when: The written grammar exists before the parser and the implementation matches it. Every function in the list has a test with a known result. A formula outside the grammar produces a clear parse error naming the position. A formula referencing a missing column produces a named error rather than a null. Do not build yet: No recomputation strategy yet, so evaluation may be naive. ### Phase 3 · Recomputing only what changed Build: Build a dependency graph from formulas. On a cell change, recompute only the dependents in topological order. Detect cycles when a formula is saved and refuse it with the cycle named. Test with a table large enough to make a naive approach obviously wrong. Trap: 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. Done when: Editing one cell in a five thousand row table recomputes only its dependents, verified by counting evaluations. A formula creating a cycle is refused at save time with the cycle named. Recomputation order is correct for a chain of three dependent formula columns. A full recompute of the large table completes in a stated time. Do not build yet: No views or buttons yet. ### Phase 4 · Views over one table Build: Views with filters, grouping and sorting, evaluated as queries against the table. Editing a row through a view edits the table. A row that no longer matches a filter leaves the view rather than becoming a stale copy. Trap: 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. Done when: Editing a row through a filtered view updates the underlying table. A row edited so it no longer matches the filter disappears from that view and remains in the table. Grouping shows correct counts per group. Two views of one table with different filters both reflect an edit made in either. Do not build yet: No buttons or automations yet. ### Phase 5 · Buttons that do a fixed set of things Build: A closed set of button actions: add a row, set a value, and call a webhook. Each is configured rather than scripted. Log every press with what it did. Confirm before an action that affects more than a threshold of rows. Trap: 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. Done when: Each action kind works and is logged with what it changed. An action affecting more rows than the threshold requires confirmation. There is no code path that executes user-supplied script, asserted by a test. A webhook action's failure is visible rather than silent. Do not build yet: No sharing or permissions yet. ### Phase 6 · Sharing, permissions and export Build: Per-document permissions of view, comment and edit, enforced on the server. Export a document as Markdown with tables as CSV alongside. Import the same back. Then confirm the export is usable without this application. Trap: 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. Done when: A viewer cannot write, tested by calling the write endpoint directly with viewer credentials. A document exports as Markdown with tables as CSV files. The export re-imports and produces the same tables and formulas. A shared document's buttons respect the viewer's permission. Do not build yet: Nothing after this. ### Out of scope (and why) - A general purpose formula language. The commercial product's language is large, well documented and consistent, and matching it is a multi-year project that rule zero exists to prevent you starting. - The pack ecosystem that connects documents to other services, which is other people's integration work. - Real-time collaborative editing of documents and tables together, which is meaningfully harder than either alone. - The permission model a company needs, with groups, inheritance and sharing policy. ### README must contain - That the formula grammar is written down and closed, and where the file is - That tables live once and blocks reference them, so a view is never a copy - That recomputation walks a dependency graph rather than re-evaluating the table - That button actions are a closed configured set with no scripting surface - That permissions are enforced in handlers rather than in the interface ===== ARCHITECTURE.md ===== # Architecture · Coda ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with a small server and PostgreSQL | tables are data and belong in a database rather than in a document blob | | Editor | A structured document model with typed blocks | a document that embeds live tables cannot be a string of Markdown | | Formulas | A hand-written parser and evaluator over a written grammar | a grammar you wrote down is a grammar you can implement correctly; one you grew by accretion is not | | Recompute | A dependency graph with topological evaluation | recomputing every formula on every edit is fine at ten rows and unusable at a thousand | | Views | Filters and grouping over one table, never copies | two copies of the same data diverge, and reconciling them is the bug you will spend a month on | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Document model | Blocks and their references to tables. | The reference rather than embed decision is what keeps data from diverging and should survive any change. | | Formula | Grammar, parsing and evaluation. | Replaceable, provided the replacement is also written down first and also closed. | | Dependency graph | What recomputes when. | Any incremental strategy. Full recomputation is the thing to avoid, at any scale you would actually use. | | Views | Filtered and grouped queries over a table. | Must remain queries. A materialised view reintroduces the divergence problem the whole design avoids. | | Buttons | A closed set of configured actions. | Adding an action kind is fine. Adding a scripting surface changes the security model of every shared document. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Your PostgreSQL connection string. - `BUTTON_CONFIRM_ROWS` · required · Above this many affected rows, a button press requires confirmation. - `MAX_FORMULA_DEPTH` · required · Maximum dependency chain length. A cap that catches pathological documents before they hang. - `RECOMPUTE_BUDGET_MS` · required · Soft budget for a recomputation pass. Exceeding it is logged, since it means the graph has grown. - `EXPORT_DIR` · required · Where document exports and their CSV tables are written. ## 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 · Coda product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with a small server and PostgreSQL, A structured document model with typed blocks, A hand-written parser and evaluator over a written grammar, A dependency graph with topological evaluation, Filters and grouping over one table, never copies. - 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 - 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. - 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 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. - 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. - 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. - 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. ===== MILESTONES.md ===== # Delivery milestones · Coda Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Documents with real tables in them A table inside a document is data, and editing a cell in one place shows everywhere. ### Steps 1. Model blocks and tables separately A block references a table. The table is the data and lives once. Files: `src/model.ts` 2. Type and validate columns Number, date, select and reference all reject invalid input at the edge rather than storing it and failing later. Files: `src/columns.ts` 3. Handle shared references Deleting a document must not orphan a table that another document uses. Files: `src/tables.ts` ### Done when - [ ] Two blocks referencing one table stay in sync - [ ] A number column rejects text with a visible error - [ ] Row order persists - [ ] Deleting a document does not delete a referenced table ### 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. ## M2 · A formula language you wrote down first Formulas evaluate correctly, and the language is exactly as large as you specified. ### Steps 1. Write the grammar In a file, before any parser code. Literals, references, operators and a closed function list. Files: `docs/formula-grammar.md` 2. Implement parser and evaluator against it Anything not in the grammar is a parse error, not a special case. Files: `src/formula/parse.ts`, `src/formula/eval.ts` 3. Test every function 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 - [ ] The grammar file predates the parser and matches it - [ ] Every function has a test with a known result - [ ] An out-of-grammar formula gives a positioned parse error - [ ] A missing column reference gives a named error, not a null ### 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. ## M3 · Recomputing only what changed Editing one cell in a large table is instant. ### Steps 1. Maintain the dependency graph Updated when a formula changes, not recomputed by scanning on every edit. Files: `src/deps.ts` 2. Evaluate topologically Only dependents of the changed cell, in order. Count evaluations in a test. Files: `src/recompute.ts` 3. Detect cycles at save 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 - [ ] One cell edit recomputes only dependents, proved by evaluation count - [ ] A cyclic formula is refused at save with the cycle named - [ ] A three-deep formula chain evaluates in the right order - [ ] Full recompute of a five thousand row table completes in a stated time ### 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. ## M4 · Views over one table The same data appears filtered and grouped in several places without ever being copied. ### Steps 1. Evaluate views as queries Filter, group and sort computed at read time against the table. Files: `src/views.ts` 2. Make edits write through A view is a lens. Editing through it edits the table, with no intermediate copy. Files: `src/views.ts` 3. Handle rows leaving a view 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 - [ ] Editing through a view updates the table - [ ] A row that stops matching leaves the view and stays in the table - [ ] Group counts are correct - [ ] Two views reflect an edit made in either ### 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. ## M5 · Buttons that do a fixed set of things A button in a document performs a defined action, and its blast radius is known. ### Steps 1. Implement the closed action set Three kinds, configured through a form. No scripting surface anywhere. Files: `src/buttons.ts` 2. Log every press Who, when, what changed. A button that altered forty rows must be traceable. Files: `src/audit.ts` 3. Confirm above a threshold The protection against a misconfigured filter plus a set-value action. Files: `src/buttons.ts` ### Done when - [ ] Each action kind works and is logged - [ ] A large action requires confirmation - [ ] No code path executes user script, asserted by test - [ ] A failed webhook is visible ### 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. ## M6 · Sharing, permissions and export (production only) Someone else can open a document without being able to break it, and nothing is trapped. ### Steps 1. Enforce permissions server side In every handler. Test with a direct request rather than through the interface. Files: `src/auth.ts` 2. Export documents and tables Markdown for prose, CSV for tables, and the formula definitions alongside so nothing is lost. Files: `src/export.ts` 3. Verify the round trip Re-import and compare. An export you cannot re-import is a screenshot. Files: `src/export.test.ts` ### Done when - [ ] A viewer's direct write request is refused - [ ] Export produces Markdown and CSV - [ ] The export re-imports to identical tables and formulas - [ ] Buttons respect the viewer's permission ### Watch 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. ===== OPERATIONS.md ===== # Operations · Coda ## Backup The database holds documents, tables and formulas together, and a table without its formulas is a spreadsheet of stale values. Back up the whole thing. ## Restore Restore and open a document with formulas. A full recompute on load confirms the formulas survived, which is the check that matters. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Recomputation time against the budget, and dependency graph depth. Both grow quietly as documents accumulate columns, and the budget breach is the early warning. ## Incident checklist A cyclic formula that reached the database despite the save check will hang recomputation, which is why the depth cap exists as a second line. If it happens, the cycle detector missed a case worth adding to its tests. 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 - [ ] The grammar file matches the implemented parser - [ ] Every formula function has a passing test - [ ] One cell edit is confirmed to recompute only dependents - [ ] No user script execution path exists, asserted by test - [ ] A viewer's direct write request is confirmed refused ## Launch constraint Do not market omitted Coda 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 · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/docs # Required. Above this many affected rows, a button press requires confirmation. BUTTON_CONFIRM_ROWS=25 # Required. Maximum dependency chain length. A cap that catches pathological documents before they hang. MAX_FORMULA_DEPTH=20 # Required. Soft budget for a recomputation pass. Exceeding it is logged, since it means the graph has grown. RECOMPUTE_BUDGET_MS=500 # Required. Where document exports and their CSV tables are written. EXPORT_DIR=./exports
# Coda · indie build 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-day**. 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 | TypeScript with a small server and PostgreSQL | tables are data and belong in a database rather than in a document blob | | Editor | A structured document model with typed blocks | a document that embeds live tables cannot be a string of Markdown | | Formulas | A hand-written parser and evaluator over a written grammar | a grammar you wrote down is a grammar you can implement correctly; one you grew by accretion is not | | Recompute | A dependency graph with topological evaluation | recomputing every formula on every edit is fine at ten rows and unusable at a thousand | | Views | Filters and grouping over one table, never copies | two copies of the same data diverge, and reconciling them is the bug you will spend a month on | ## 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 server and the build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free - Why: Holds documents, tables and rows. A document store would make querying views painful. - Get it: Install locally or run it in Docker. - Verify: psql connects and reports 16 or higher - [ ] **One real document you actually need** · free - 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 - [ ] **Willingness to write a grammar before code** · free - 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 - [ ] **A generated table of five thousand rows** · free - 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 ## 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: - 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 If one of those is essential to you, that is the reason to keep paying for Coda, and the README should say so rather than pretend.
# Build brief · Coda 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 document that contains real tables, a formula language and buttons that do things to replace Coda. 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. ### Rule zero The temptation here is to build a general purpose formula language, and that is where this project dies. A formula language is a parser, an evaluator, a dependency graph, a cycle detector and an error model, and every one of them has to be right before a single formula is trustworthy. So build the smallest formula language that does your actual work, define its grammar in writing before you implement it, and refuse to grow it by adding special cases. If you need general computation, the honest answer is a spreadsheet next to a document rather than one tool pretending to be both. ### Stack (fixed, do not substitute) - Runtime: TypeScript with a small server and PostgreSQL · tables are data and belong in a database rather than in a document blob - Editor: A structured document model with typed blocks · a document that embeds live tables cannot be a string of Markdown - Formulas: A hand-written parser and evaluator over a written grammar · a grammar you wrote down is a grammar you can implement correctly; one you grew by accretion is not - Recompute: A dependency graph with topological evaluation · recomputing every formula on every edit is fine at ten rows and unusable at a thousand - Views: Filters and grouping over one table, never copies · two copies of the same data diverge, and reconciling them is the bug you will spend a month on ### Data model (create this before Phase 1) - 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. ### Phase 1 · Documents with real tables in them Build: A block-based document model with typed blocks. A table block referencing a table rather than containing it. Typed columns with validation. Two blocks referencing the same table show the same data. Trap: 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. Done when: Two table blocks in different documents referencing one table show the same rows, and editing through either updates both. A number column rejects text with a visible error rather than storing it. Reordering rows persists. Deleting a document does not delete a table another document references. Do not build yet: No formulas, no views, no buttons. ### Phase 2 · A formula language you wrote down first Build: Write the grammar before implementing: literals, column references, arithmetic, comparison, and a fixed list of functions. Then write the parser and evaluator against it. Reject anything outside the grammar with a clear error rather than guessing. Trap: 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. Done when: The written grammar exists before the parser and the implementation matches it. Every function in the list has a test with a known result. A formula outside the grammar produces a clear parse error naming the position. A formula referencing a missing column produces a named error rather than a null. Do not build yet: No recomputation strategy yet, so evaluation may be naive. ### Phase 3 · Recomputing only what changed Build: Build a dependency graph from formulas. On a cell change, recompute only the dependents in topological order. Detect cycles when a formula is saved and refuse it with the cycle named. Test with a table large enough to make a naive approach obviously wrong. Trap: 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. Done when: Editing one cell in a five thousand row table recomputes only its dependents, verified by counting evaluations. A formula creating a cycle is refused at save time with the cycle named. Recomputation order is correct for a chain of three dependent formula columns. A full recompute of the large table completes in a stated time. Do not build yet: No views or buttons yet. ### Phase 4 · Views over one table Build: Views with filters, grouping and sorting, evaluated as queries against the table. Editing a row through a view edits the table. A row that no longer matches a filter leaves the view rather than becoming a stale copy. Trap: 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. Done when: Editing a row through a filtered view updates the underlying table. A row edited so it no longer matches the filter disappears from that view and remains in the table. Grouping shows correct counts per group. Two views of one table with different filters both reflect an edit made in either. Do not build yet: No buttons or automations yet. ### Phase 5 · Buttons that do a fixed set of things Build: A closed set of button actions: add a row, set a value, and call a webhook. Each is configured rather than scripted. Log every press with what it did. Confirm before an action that affects more than a threshold of rows. Trap: 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. Done when: Each action kind works and is logged with what it changed. An action affecting more rows than the threshold requires confirmation. There is no code path that executes user-supplied script, asserted by a test. A webhook action's failure is visible rather than silent. Do not build yet: No sharing or permissions yet. ### Phase 6 · Sharing, permissions and export Build: Per-document permissions of view, comment and edit, enforced on the server. Export a document as Markdown with tables as CSV alongside. Import the same back. Then confirm the export is usable without this application. Trap: 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. Done when: A viewer cannot write, tested by calling the write endpoint directly with viewer credentials. A document exports as Markdown with tables as CSV files. The export re-imports and produces the same tables and formulas. A shared document's buttons respect the viewer's permission. Do not build yet: Nothing after this. ### Out of scope (and why) - A general purpose formula language. The commercial product's language is large, well documented and consistent, and matching it is a multi-year project that rule zero exists to prevent you starting. - The pack ecosystem that connects documents to other services, which is other people's integration work. - Real-time collaborative editing of documents and tables together, which is meaningfully harder than either alone. - The permission model a company needs, with groups, inheritance and sharing policy. ### README must contain - That the formula grammar is written down and closed, and where the file is - That tables live once and blocks reference them, so a view is never a copy - That recomputation walks a dependency graph rather than re-evaluating the table - That button actions are a closed configured set with no scripting surface - That permissions are enforced in handlers rather than in the interface
# Agent instructions · Coda indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with a small server and PostgreSQL, A structured document model with typed blocks, A hand-written parser and evaluator over a written grammar, A dependency graph with topological evaluation, Filters and grouping over one table, never copies. 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 - 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. - 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 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. - 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. - 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.
# Build plan · Coda 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Documents with real tables in them A table inside a document is data, and editing a cell in one place shows everywhere. ### Steps 1. Model blocks and tables separately A block references a table. The table is the data and lives once. Files: `src/model.ts` 2. Type and validate columns Number, date, select and reference all reject invalid input at the edge rather than storing it and failing later. Files: `src/columns.ts` 3. Handle shared references Deleting a document must not orphan a table that another document uses. Files: `src/tables.ts` ### Done when - [ ] Two blocks referencing one table stay in sync - [ ] A number column rejects text with a visible error - [ ] Row order persists - [ ] Deleting a document does not delete a referenced table ### 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. ## Phase 2 · A formula language you wrote down first Formulas evaluate correctly, and the language is exactly as large as you specified. ### Steps 1. Write the grammar In a file, before any parser code. Literals, references, operators and a closed function list. Files: `docs/formula-grammar.md` 2. Implement parser and evaluator against it Anything not in the grammar is a parse error, not a special case. Files: `src/formula/parse.ts`, `src/formula/eval.ts` 3. Test every function 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 - [ ] The grammar file predates the parser and matches it - [ ] Every function has a test with a known result - [ ] An out-of-grammar formula gives a positioned parse error - [ ] A missing column reference gives a named error, not a null ### 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. ## Phase 3 · Recomputing only what changed Editing one cell in a large table is instant. ### Steps 1. Maintain the dependency graph Updated when a formula changes, not recomputed by scanning on every edit. Files: `src/deps.ts` 2. Evaluate topologically Only dependents of the changed cell, in order. Count evaluations in a test. Files: `src/recompute.ts` 3. Detect cycles at save 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 - [ ] One cell edit recomputes only dependents, proved by evaluation count - [ ] A cyclic formula is refused at save with the cycle named - [ ] A three-deep formula chain evaluates in the right order - [ ] Full recompute of a five thousand row table completes in a stated time ### 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. ## Phase 4 · Views over one table The same data appears filtered and grouped in several places without ever being copied. ### Steps 1. Evaluate views as queries Filter, group and sort computed at read time against the table. Files: `src/views.ts` 2. Make edits write through A view is a lens. Editing through it edits the table, with no intermediate copy. Files: `src/views.ts` 3. Handle rows leaving a view 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 - [ ] Editing through a view updates the table - [ ] A row that stops matching leaves the view and stays in the table - [ ] Group counts are correct - [ ] Two views reflect an edit made in either ### 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. ## Phase 5 · Buttons that do a fixed set of things A button in a document performs a defined action, and its blast radius is known. ### Steps 1. Implement the closed action set Three kinds, configured through a form. No scripting surface anywhere. Files: `src/buttons.ts` 2. Log every press Who, when, what changed. A button that altered forty rows must be traceable. Files: `src/audit.ts` 3. Confirm above a threshold The protection against a misconfigured filter plus a set-value action. Files: `src/buttons.ts` ### Done when - [ ] Each action kind works and is logged - [ ] A large action requires confirmation - [ ] No code path executes user script, asserted by test - [ ] A failed webhook is visible ### 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. ## Not in this build - A general purpose formula language. - The pack and integration ecosystem. - Real-time collaborative editing. - An organisational permission model. ## After v1, if you want it - 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
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/docs # Required. Above this many affected rows, a button press requires confirmation. BUTTON_CONFIRM_ROWS=25 # Required. Maximum dependency chain length. A cap that catches pathological documents before they hang. MAX_FORMULA_DEPTH=20 # Required. Soft budget for a recomputation pass. Exceeding it is logged, since it means the graph has grown. RECOMPUTE_BUDGET_MS=500 # Required. Where document exports and their CSV tables are written. EXPORT_DIR=./exports
# Coda · product brief ## Problem A doc with tables and buttons is buildable; Coda's editor, formula engine, packs/integrations, permissions, and team workflows make full parity difficult. ## Product outcome A document tool whose formula language is small and specified, whose tables have one home, whose recomputation is proportional to what changed, and whose buttons cannot run arbitrary code. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - rich text editor - database tables - formula engine - auth - integrations - hosted backend ## Explicit non-goals for v1 - 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 ## Success criteria - The grammar file matches the implemented parser - Every formula function has a passing test - One cell edit is confirmed to recompute only dependents - No user script execution path exists, asserted by test - A viewer's direct write request is confirmed refused
# Build brief · Coda 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 document that contains real tables, a formula language and buttons that do things to replace Coda. 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. ### Rule zero The temptation here is to build a general purpose formula language, and that is where this project dies. A formula language is a parser, an evaluator, a dependency graph, a cycle detector and an error model, and every one of them has to be right before a single formula is trustworthy. So build the smallest formula language that does your actual work, define its grammar in writing before you implement it, and refuse to grow it by adding special cases. If you need general computation, the honest answer is a spreadsheet next to a document rather than one tool pretending to be both. ### Stack (fixed, do not substitute) - Runtime: TypeScript with a small server and PostgreSQL · tables are data and belong in a database rather than in a document blob - Editor: A structured document model with typed blocks · a document that embeds live tables cannot be a string of Markdown - Formulas: A hand-written parser and evaluator over a written grammar · a grammar you wrote down is a grammar you can implement correctly; one you grew by accretion is not - Recompute: A dependency graph with topological evaluation · recomputing every formula on every edit is fine at ten rows and unusable at a thousand - Views: Filters and grouping over one table, never copies · two copies of the same data diverge, and reconciling them is the bug you will spend a month on ### Data model (create this before Phase 1) - 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. ### Phase 1 · Documents with real tables in them Build: A block-based document model with typed blocks. A table block referencing a table rather than containing it. Typed columns with validation. Two blocks referencing the same table show the same data. Trap: 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. Done when: Two table blocks in different documents referencing one table show the same rows, and editing through either updates both. A number column rejects text with a visible error rather than storing it. Reordering rows persists. Deleting a document does not delete a table another document references. Do not build yet: No formulas, no views, no buttons. ### Phase 2 · A formula language you wrote down first Build: Write the grammar before implementing: literals, column references, arithmetic, comparison, and a fixed list of functions. Then write the parser and evaluator against it. Reject anything outside the grammar with a clear error rather than guessing. Trap: 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. Done when: The written grammar exists before the parser and the implementation matches it. Every function in the list has a test with a known result. A formula outside the grammar produces a clear parse error naming the position. A formula referencing a missing column produces a named error rather than a null. Do not build yet: No recomputation strategy yet, so evaluation may be naive. ### Phase 3 · Recomputing only what changed Build: Build a dependency graph from formulas. On a cell change, recompute only the dependents in topological order. Detect cycles when a formula is saved and refuse it with the cycle named. Test with a table large enough to make a naive approach obviously wrong. Trap: 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. Done when: Editing one cell in a five thousand row table recomputes only its dependents, verified by counting evaluations. A formula creating a cycle is refused at save time with the cycle named. Recomputation order is correct for a chain of three dependent formula columns. A full recompute of the large table completes in a stated time. Do not build yet: No views or buttons yet. ### Phase 4 · Views over one table Build: Views with filters, grouping and sorting, evaluated as queries against the table. Editing a row through a view edits the table. A row that no longer matches a filter leaves the view rather than becoming a stale copy. Trap: 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. Done when: Editing a row through a filtered view updates the underlying table. A row edited so it no longer matches the filter disappears from that view and remains in the table. Grouping shows correct counts per group. Two views of one table with different filters both reflect an edit made in either. Do not build yet: No buttons or automations yet. ### Phase 5 · Buttons that do a fixed set of things Build: A closed set of button actions: add a row, set a value, and call a webhook. Each is configured rather than scripted. Log every press with what it did. Confirm before an action that affects more than a threshold of rows. Trap: 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. Done when: Each action kind works and is logged with what it changed. An action affecting more rows than the threshold requires confirmation. There is no code path that executes user-supplied script, asserted by a test. A webhook action's failure is visible rather than silent. Do not build yet: No sharing or permissions yet. ### Phase 6 · Sharing, permissions and export Build: Per-document permissions of view, comment and edit, enforced on the server. Export a document as Markdown with tables as CSV alongside. Import the same back. Then confirm the export is usable without this application. Trap: 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. Done when: A viewer cannot write, tested by calling the write endpoint directly with viewer credentials. A document exports as Markdown with tables as CSV files. The export re-imports and produces the same tables and formulas. A shared document's buttons respect the viewer's permission. Do not build yet: Nothing after this. ### Out of scope (and why) - A general purpose formula language. The commercial product's language is large, well documented and consistent, and matching it is a multi-year project that rule zero exists to prevent you starting. - The pack ecosystem that connects documents to other services, which is other people's integration work. - Real-time collaborative editing of documents and tables together, which is meaningfully harder than either alone. - The permission model a company needs, with groups, inheritance and sharing policy. ### README must contain - That the formula grammar is written down and closed, and where the file is - That tables live once and blocks reference them, so a view is never a copy - That recomputation walks a dependency graph rather than re-evaluating the table - That button actions are a closed configured set with no scripting surface - That permissions are enforced in handlers rather than in the interface
# Architecture · Coda ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with a small server and PostgreSQL | tables are data and belong in a database rather than in a document blob | | Editor | A structured document model with typed blocks | a document that embeds live tables cannot be a string of Markdown | | Formulas | A hand-written parser and evaluator over a written grammar | a grammar you wrote down is a grammar you can implement correctly; one you grew by accretion is not | | Recompute | A dependency graph with topological evaluation | recomputing every formula on every edit is fine at ten rows and unusable at a thousand | | Views | Filters and grouping over one table, never copies | two copies of the same data diverge, and reconciling them is the bug you will spend a month on | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Document model | Blocks and their references to tables. | The reference rather than embed decision is what keeps data from diverging and should survive any change. | | Formula | Grammar, parsing and evaluation. | Replaceable, provided the replacement is also written down first and also closed. | | Dependency graph | What recomputes when. | Any incremental strategy. Full recomputation is the thing to avoid, at any scale you would actually use. | | Views | Filtered and grouped queries over a table. | Must remain queries. A materialised view reintroduces the divergence problem the whole design avoids. | | Buttons | A closed set of configured actions. | Adding an action kind is fine. Adding a scripting surface changes the security model of every shared document. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Your PostgreSQL connection string. - `BUTTON_CONFIRM_ROWS` · required · Above this many affected rows, a button press requires confirmation. - `MAX_FORMULA_DEPTH` · required · Maximum dependency chain length. A cap that catches pathological documents before they hang. - `RECOMPUTE_BUDGET_MS` · required · Soft budget for a recomputation pass. Exceeding it is logged, since it means the graph has grown. - `EXPORT_DIR` · required · Where document exports and their CSV tables are written. ## 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 · Coda product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with a small server and PostgreSQL, A structured document model with typed blocks, A hand-written parser and evaluator over a written grammar, A dependency graph with topological evaluation, Filters and grouping over one table, never copies. - 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 - 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. - 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 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. - 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. - 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. - 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.
# Delivery milestones · Coda Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Documents with real tables in them A table inside a document is data, and editing a cell in one place shows everywhere. ### Steps 1. Model blocks and tables separately A block references a table. The table is the data and lives once. Files: `src/model.ts` 2. Type and validate columns Number, date, select and reference all reject invalid input at the edge rather than storing it and failing later. Files: `src/columns.ts` 3. Handle shared references Deleting a document must not orphan a table that another document uses. Files: `src/tables.ts` ### Done when - [ ] Two blocks referencing one table stay in sync - [ ] A number column rejects text with a visible error - [ ] Row order persists - [ ] Deleting a document does not delete a referenced table ### 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. ## M2 · A formula language you wrote down first Formulas evaluate correctly, and the language is exactly as large as you specified. ### Steps 1. Write the grammar In a file, before any parser code. Literals, references, operators and a closed function list. Files: `docs/formula-grammar.md` 2. Implement parser and evaluator against it Anything not in the grammar is a parse error, not a special case. Files: `src/formula/parse.ts`, `src/formula/eval.ts` 3. Test every function 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 - [ ] The grammar file predates the parser and matches it - [ ] Every function has a test with a known result - [ ] An out-of-grammar formula gives a positioned parse error - [ ] A missing column reference gives a named error, not a null ### 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. ## M3 · Recomputing only what changed Editing one cell in a large table is instant. ### Steps 1. Maintain the dependency graph Updated when a formula changes, not recomputed by scanning on every edit. Files: `src/deps.ts` 2. Evaluate topologically Only dependents of the changed cell, in order. Count evaluations in a test. Files: `src/recompute.ts` 3. Detect cycles at save 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 - [ ] One cell edit recomputes only dependents, proved by evaluation count - [ ] A cyclic formula is refused at save with the cycle named - [ ] A three-deep formula chain evaluates in the right order - [ ] Full recompute of a five thousand row table completes in a stated time ### 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. ## M4 · Views over one table The same data appears filtered and grouped in several places without ever being copied. ### Steps 1. Evaluate views as queries Filter, group and sort computed at read time against the table. Files: `src/views.ts` 2. Make edits write through A view is a lens. Editing through it edits the table, with no intermediate copy. Files: `src/views.ts` 3. Handle rows leaving a view 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 - [ ] Editing through a view updates the table - [ ] A row that stops matching leaves the view and stays in the table - [ ] Group counts are correct - [ ] Two views reflect an edit made in either ### 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. ## M5 · Buttons that do a fixed set of things A button in a document performs a defined action, and its blast radius is known. ### Steps 1. Implement the closed action set Three kinds, configured through a form. No scripting surface anywhere. Files: `src/buttons.ts` 2. Log every press Who, when, what changed. A button that altered forty rows must be traceable. Files: `src/audit.ts` 3. Confirm above a threshold The protection against a misconfigured filter plus a set-value action. Files: `src/buttons.ts` ### Done when - [ ] Each action kind works and is logged - [ ] A large action requires confirmation - [ ] No code path executes user script, asserted by test - [ ] A failed webhook is visible ### 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. ## M6 · Sharing, permissions and export (production only) Someone else can open a document without being able to break it, and nothing is trapped. ### Steps 1. Enforce permissions server side In every handler. Test with a direct request rather than through the interface. Files: `src/auth.ts` 2. Export documents and tables Markdown for prose, CSV for tables, and the formula definitions alongside so nothing is lost. Files: `src/export.ts` 3. Verify the round trip Re-import and compare. An export you cannot re-import is a screenshot. Files: `src/export.test.ts` ### Done when - [ ] A viewer's direct write request is refused - [ ] Export produces Markdown and CSV - [ ] The export re-imports to identical tables and formulas - [ ] Buttons respect the viewer's permission ### Watch 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.
# Operations · Coda ## Backup The database holds documents, tables and formulas together, and a table without its formulas is a spreadsheet of stale values. Back up the whole thing. ## Restore Restore and open a document with formulas. A full recompute on load confirms the formulas survived, which is the check that matters. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Recomputation time against the budget, and dependency graph depth. Both grow quietly as documents accumulate columns, and the budget breach is the early warning. ## Incident checklist A cyclic formula that reached the database despite the save check will hang recomputation, which is why the depth cap exists as a second line. If it happens, the cycle detector missed a case worth adding to its tests. 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 - [ ] The grammar file matches the implemented parser - [ ] Every formula function has a passing test - [ ] One cell edit is confirmed to recompute only dependents - [ ] No user script execution path exists, asserted by test - [ ] A viewer's direct write request is confirmed refused ## Launch constraint Do not market omitted Coda 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 · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/docs # Required. Above this many affected rows, a button press requires confirmation. BUTTON_CONFIRM_ROWS=25 # Required. Maximum dependency chain length. A cap that catches pathological documents before they hang. MAX_FORMULA_DEPTH=20 # Required. Soft budget for a recomputation pass. Exceeding it is logged, since it means the graph has grown. RECOMPUTE_BUDGET_MS=500 # Required. Where document exports and their CSV tables are written. EXPORT_DIR=./exports
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because docs become tools that teammates can understand and maintain.
xformula language
xpacks
xpermissions
xautomations
xtemplates
xcollaboration
xdoc-app UX
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Coda pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/workspace | $0/workspace | Personal docs have no object/page cap; shared docs cap at 50 objects and 1,000 rows; 1 GB attachments/doc, 10 MB/file, 7-day history, 35 time + 100 event automations/doc/month, 100 rows/sync table |
| pro | $15/user | $12/user | Per Doc Maker; unlimited doc size; 5 GB attachments/doc, 250 MB/file, 30-day history, 100 time + 500 event automations/doc/month, 2,000 AI credits/Doc Maker/month, 10,000 rows/sync table |
| business | $40/user | $33/user | Per Doc Maker; unlimited attachments/doc and history; 250 MB/file; unlimited automations; 6,000 AI credits/Doc Maker/month; 10,000 rows/sync table |
| enterprise | custom | — | Custom contract; 12,000 AI credits/Doc Maker/month; unlimited attachments/doc, history and automations; 250 MB/file |
free tierPersonal docs have no object/page cap; shared docs: 50 objects and 1,000 rows; 1 GB attachments/doc, 10 MB/file, 7-day history, 35 time + 100 event automations/doc/month, 100 rows/sync table
billingmonthly + annual; Pro $15 monthly or $12/Doc Maker/month annually; Business $40 monthly or $33 annually; editors/viewers are free
hidden costsWorkspace admins and Doc Makers are billable; editors are free. Depending on admin settings, an editor who creates a doc/page or uses AI can become a billable Doc Maker after a 7- or 14-day grace/trial. Paid Packs are extra per Doc Maker per month with no annual option (for example, the Figma Pack is $10/Doc Maker/month). Annual-plan seat additions are prorated and billed monthly.
verified 2026-08-13 · source ↗
Vibecode Coda
Kinda. The core of Coda is buildable in a weekend with the prompt on this page, but there are real gaps: formula language, packs. Read the honest list above before committing.
How much does Coda cost?
Coda costs about $12/month (Pro Doc Maker, checked 2026-07-30), which is $144 per year.
What do I lose by replacing Coda?
Honestly: formula language; packs; permissions; automations; templates; collaboration; doc-app UX. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Coda?
Yes: Fibery Free (Coda for ten people, with relational docs and 250 automations before the bill exists.) Superhuman Docs Free (Coda is now Superhuman Docs; the free plan still has collaborative docs, connected tables, forms, formulas, and automations.) The prompt is for when you want it exactly your way.