Vibecode NocoDB Cloud
track this build4 phases, 12 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For NocoDB Cloud, use a managed spreadsheet interface over structured data. The hard boundary is managed hosting, integrations, collaboration, storage, and support, plus data model flexibility, collaboration, and integrations.
You are building a lean indie version of NocoDB Cloud. 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 ===== # NocoDB Cloud · indie build A no-code database where the difficulty is that users design the schema at runtime: every column type change is a migration on live data, every formula is code someone typed into a cell, and every shared view is a permission decision that has to hold at the row level. 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 | | --- | --- | --- | | Storage | Real tables in PostgreSQL, one per user table | a key-value store makes every query slow and every constraint impossible | | Schema | User-driven migrations with a preview and a reversible path | a type change on live data is a migration, whatever the interface calls it | | Formulas | A parsed expression language, evaluated in a sandbox | a formula is user-supplied code and must not be able to reach anything | | Views | Filters, sorts and field visibility as saved objects with their own permissions | a shared view is the sharing mechanism and must not leak hidden fields | | Realtime | Change broadcast per table with per-row permission filtering | a live update must not deliver a row the recipient cannot see | ## 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, the formula parser and the realtime layer. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: User tables are real tables, which needs a database that handles runtime schema changes without long locks. - Get it: Docker or your package manager. - Verify: A column type change on a large table completes without an unacceptable lock - [ ] **A parser generator or hand-written parser plan** · free - Why: The formula language is the security boundary, and reaching for the language's own evaluator is how this category gets compromised. - Get it: Decide the grammar and the function allow list before writing the parser. - Verify: A written grammar exists and the function list is finite - [ ] **A real table with messy data** · free - Why: Migrations, formulas and sorting all behave differently against real data, which contains a note in a number column and a date nobody can parse. - Get it: Import a real spreadsheet you use, including the columns that have inconsistent values. - Verify: At least one column has mixed types and one has values that fail to parse - [ ] **Your sharing model, decided early** · free - Why: Row-level rules and hidden fields have to be enforced in the query, and retrofitting that after building on client-side filtering is a rewrite of the data access layer. - Get it: Decide whether row rules exist, what a share link may expose, and whether shared views can be edited. Write it down before phase 4. - Verify: A written note states the rules and what a share token grants ## 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: - Automations and workflow integrations. - External database sync connectors. - Scale beyond a few million rows per table. - A spreadsheet's full formula library. - managed hosting, integrations, collaboration, storage, and support - real-time multiplayer - automation and integration breadth - scale and formula depth - enterprise permissions If one of those is essential to you, that is the reason to keep paying for NocoDB Cloud, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · NocoDB Cloud 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 spreadsheet interface over a real database, where users define the schema to replace NocoDB Cloud. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Storage: Real tables in PostgreSQL, one per user table · a key-value store makes every query slow and every constraint impossible - Schema: User-driven migrations with a preview and a reversible path · a type change on live data is a migration, whatever the interface calls it - Formulas: A parsed expression language, evaluated in a sandbox · a formula is user-supplied code and must not be able to reach anything - Views: Filters, sorts and field visibility as saved objects with their own permissions · a shared view is the sharing mechanism and must not leak hidden fields - Realtime: Change broadcast per table with per-row permission filtering · a live update must not deliver a row the recipient cannot see ### Data model (create this before Phase 1) - bases(id, name, owner_id, created_at) - tables(id, base_id, name, physical_name) - columns(id, table_id, name, kind, options_json, physical_name, ordinal) · kind includes text, number, date, select, link and formula - migrations(id, table_id, from_json, to_json, rows_affected, applied_at, reversible_until) - formulas(column_id, expression, compiled_json, depends_on_column_ids) · dependencies, so a cycle is detectable - views(id, table_id, kind, filters_json, sorts_json, hidden_column_ids, share_token_hash, share_scope) - row_permissions(view_id, rule_json) · row-level rules, evaluated server side and never in the client - A migration records what it changed and remains reversible for a period, because a column type change on real data is not something to do optimistically. ### Phase 1 · Real tables, defined at runtime Build: Create physical tables and columns from user definitions, mapping each user type to a database type. Keep physical names separate from display names so renaming is free. Enforce types at the database rather than in the application. Trap: Storing every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. Done when: A user-created table exists as a real table with typed columns. Renaming a column does not rewrite data, since the physical name is stable. Number and date sorting is correct rather than lexicographic. A type constraint is enforced by the database, verified by attempting a bad insert directly. Do not build yet: No migrations, formulas, views or sharing. ### Phase 2 · Changing a column that already has data Build: Implement column type changes as migrations that first report how many rows would fail or lose information. Require confirmation. Keep the change reversible for a period by retaining the previous values. Trap: Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. Done when: A type change reports how many rows would fail or lose data before applying, tested on a column with mixed content. The change requires confirmation. The previous values are retained and the change is reversible for a stated period. A migration on a large table does not lock it for an unacceptable time. Do not build yet: No formulas, views or sharing yet. ### Phase 3 · Formulas, which are user-supplied code Build: Parse formulas into an expression tree rather than evaluating strings. Evaluate in a sandbox with no access to the environment, the filesystem or other tables beyond declared links. Detect dependency cycles at save time. Trap: Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. Done when: Formulas are parsed rather than evaluated as strings, verified by inspecting the implementation. A formula attempting to reach the environment fails at parse time, tested with a crafted expression. A dependency cycle is detected at save rather than at evaluation. A formula over ten thousand rows computes within an acceptable time. Do not build yet: No views or sharing yet. ### Phase 4 · Views that hide what they claim to hide Build: Views with filters, sorts, hidden fields and row-level rules, all applied server side. Verify that a hidden column never appears in an API response for that view, including in sorts, filters and error messages. Trap: Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. Done when: A hidden column is absent from every API response for that view, asserted in the suite across list, single-row and error responses. Row-level rules are applied in the query rather than after fetching. Sorting or filtering by a hidden column does not leak its values. A view cannot be edited by someone with only view access. Do not build yet: No sharing or realtime yet. ### Phase 5 · Sharing, and live updates that respect permissions Build: Share links with an unguessable token, a scope and revocation. Broadcast changes filtered per recipient's view permissions. Handle a row that becomes invisible to someone because its values changed. Trap: Broadcasting a change to everyone watching a table and letting each client filter it delivers rows the recipient is not permitted to see, and the filtering happens after the data has already arrived on their machine. Filter per recipient before sending, including working out that an edit made a row newly invisible to them. Done when: A live change is filtered per recipient before sending, verified by watching the traffic for a restricted user. A row edited so that it no longer matches a viewer's rules produces a removal rather than an update carrying the new values. A share token is unguessable, scoped and revocable immediately. Revoking a token disconnects active viewers. Do not build yet: Nothing after this. ### Out of scope (and why) - Automations and integrations, which is where these products make most of their money and is a whole workflow engine. - Importing from and syncing with external databases, which is a connector per system and never finished. - Scale beyond a few million rows per table, where the generic query patterns stop being adequate. - A formula language matching the breadth of a spreadsheet, which is decades of accumulated functions. ### README must contain - That user tables are real tables with real types, and why a generic value store fails later - That a column type change is a migration that previews its data loss and stays reversible - That formulas are parsed into an expression tree and never evaluated as strings - That hidden columns are absent from responses rather than hidden in the client - That live updates are filtered per recipient before being sent ===== AGENTS.md ===== # Agent instructions · NocoDB Cloud indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Real tables in PostgreSQL, one per user table, User-driven migrations with a preview and a reversible path, A parsed expression language, evaluated in a sandbox, Filters, sorts and field visibility as saved objects with their own permissions, Change broadcast per table with per-row permission filtering. 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 every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. - Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. - Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. - Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. ===== BUILD_PLAN.md ===== # Build plan · NocoDB Cloud A no-code database where the difficulty is that users design the schema at runtime: every column type change is a migration on live data, every formula is code someone typed into a cell, and every shared view is a permission decision that has to hold at the row level. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Real tables, defined at runtime A user-created table is a real table with real types. ### Steps 1. Map user types to real database types Not text for everything. The cost of that arrives after there is data. Files: `src/schema.ts` 2. Separate physical from display names So renaming is a metadata change rather than a migration. Files: `src/columns.ts` 3. Enforce types in the database Application validation is bypassed by every other code path. Files: `src/schema.ts` ### Done when - [ ] User tables are real tables with typed columns - [ ] Renaming does not rewrite data - [ ] Numbers and dates sort correctly - [ ] Type constraints are enforced by the database ### Watch out - Storing every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. ## Phase 2 · Changing a column that already has data A type change previews its damage before doing it. ### Steps 1. Preview failures and information loss Counts and examples, before anything changes. Files: `src/migrate.ts` 2. Retain previous values For a period. It is what makes undo real rather than aspirational. Files: `src/migrate.ts` 3. Avoid long locks A migration that locks a table for a minute is an outage. Files: `src/migrate.ts` ### Done when - [ ] A type change previews failures and loss - [ ] The change requires confirmation - [ ] Previous values are retained and reversible - [ ] A large table migrates without a long lock ### Watch out - Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. ## Phase 3 · Formulas, which are user-supplied code A formula computes correctly and cannot reach anything it should not. ### Steps 1. Parse to a restricted expression tree Never evaluate a string. This is the compromise vector for the category. Files: `src/formula/parse.ts`, `src/formula/parse.test.ts` 2. Fix the function set An allow list. Anything outside it fails at parse time. Files: `src/formula/functions.ts` 3. Detect cycles at save Not at evaluation, where it becomes a hang rather than an error. Files: `src/formula/cycles.ts` ### Done when - [ ] Formulas are parsed, never string-evaluated - [ ] A crafted escape attempt fails at parse time - [ ] Dependency cycles are detected at save - [ ] Ten thousand rows compute acceptably ### Watch out - Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. ## Phase 4 · Views that hide what they claim to hide A hidden column is absent from the response, not hidden in the client. ### Steps 1. Apply visibility in the query Not in the serialiser and certainly not in the client. Files: `src/views.ts` 2. Apply row rules in the query too Fetching then filtering means the rows were fetched. Files: `src/permissions.ts` 3. Assert absence across response shapes List, single row, errors. Errors are where fields leak. Files: `tests/hidden-fields.test.ts` ### Done when - [ ] Hidden columns are absent from every response shape - [ ] Row rules are applied in the query - [ ] Sorting by a hidden column leaks nothing - [ ] View-only access cannot edit the view ### Watch out - Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. ## Not in this build - Automations and workflow integrations. - External database sync connectors. - Scale beyond a few million rows per table. - A spreadsheet's full formula library. ## After v1, if you want it - Linked records between tables, which is the feature everyone wants next and complicates every query - Undo across a session rather than per migration - An audit log per row, once the permission model is provably correct ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Metadata and the user tables themselves. DATABASE_URL=postgres://localhost:5432/nocode # Required. How long previous values are kept after a type change. The undo window. MIGRATION_REVERSIBLE_DAYS=14 # Required. Present as a constant so the answer is visible. Parsed trees only. FORMULA_EVAL_STRINGS=never # Required. A formula that will not finish must not hold a request. FORMULA_TIMEOUT_MS=500 # Required. Share links are bearer credentials. Unguessable, hashed at rest. SHARE_TOKEN_BYTES=32 # Required. Where the generic query patterns stop being adequate, made explicit rather than discovered. MAX_ROWS_PER_TABLE=1000000
You are building a lean indie version of NocoDB Cloud. 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 ===== # NocoDB Cloud · indie build A no-code database where the difficulty is that users design the schema at runtime: every column type change is a migration on live data, every formula is code someone typed into a cell, and every shared view is a permission decision that has to hold at the row level. 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 | | --- | --- | --- | | Storage | Real tables in PostgreSQL, one per user table | a key-value store makes every query slow and every constraint impossible | | Schema | User-driven migrations with a preview and a reversible path | a type change on live data is a migration, whatever the interface calls it | | Formulas | A parsed expression language, evaluated in a sandbox | a formula is user-supplied code and must not be able to reach anything | | Views | Filters, sorts and field visibility as saved objects with their own permissions | a shared view is the sharing mechanism and must not leak hidden fields | | Realtime | Change broadcast per table with per-row permission filtering | a live update must not deliver a row the recipient cannot see | ## 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, the formula parser and the realtime layer. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: User tables are real tables, which needs a database that handles runtime schema changes without long locks. - Get it: Docker or your package manager. - Verify: A column type change on a large table completes without an unacceptable lock - [ ] **A parser generator or hand-written parser plan** · free - Why: The formula language is the security boundary, and reaching for the language's own evaluator is how this category gets compromised. - Get it: Decide the grammar and the function allow list before writing the parser. - Verify: A written grammar exists and the function list is finite - [ ] **A real table with messy data** · free - Why: Migrations, formulas and sorting all behave differently against real data, which contains a note in a number column and a date nobody can parse. - Get it: Import a real spreadsheet you use, including the columns that have inconsistent values. - Verify: At least one column has mixed types and one has values that fail to parse - [ ] **Your sharing model, decided early** · free - Why: Row-level rules and hidden fields have to be enforced in the query, and retrofitting that after building on client-side filtering is a rewrite of the data access layer. - Get it: Decide whether row rules exist, what a share link may expose, and whether shared views can be edited. Write it down before phase 4. - Verify: A written note states the rules and what a share token grants ## 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: - Automations and workflow integrations. - External database sync connectors. - Scale beyond a few million rows per table. - A spreadsheet's full formula library. - managed hosting, integrations, collaboration, storage, and support - real-time multiplayer - automation and integration breadth - scale and formula depth - enterprise permissions If one of those is essential to you, that is the reason to keep paying for NocoDB Cloud, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · NocoDB Cloud 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 spreadsheet interface over a real database, where users define the schema to replace NocoDB Cloud. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Storage: Real tables in PostgreSQL, one per user table · a key-value store makes every query slow and every constraint impossible - Schema: User-driven migrations with a preview and a reversible path · a type change on live data is a migration, whatever the interface calls it - Formulas: A parsed expression language, evaluated in a sandbox · a formula is user-supplied code and must not be able to reach anything - Views: Filters, sorts and field visibility as saved objects with their own permissions · a shared view is the sharing mechanism and must not leak hidden fields - Realtime: Change broadcast per table with per-row permission filtering · a live update must not deliver a row the recipient cannot see ### Data model (create this before Phase 1) - bases(id, name, owner_id, created_at) - tables(id, base_id, name, physical_name) - columns(id, table_id, name, kind, options_json, physical_name, ordinal) · kind includes text, number, date, select, link and formula - migrations(id, table_id, from_json, to_json, rows_affected, applied_at, reversible_until) - formulas(column_id, expression, compiled_json, depends_on_column_ids) · dependencies, so a cycle is detectable - views(id, table_id, kind, filters_json, sorts_json, hidden_column_ids, share_token_hash, share_scope) - row_permissions(view_id, rule_json) · row-level rules, evaluated server side and never in the client - A migration records what it changed and remains reversible for a period, because a column type change on real data is not something to do optimistically. ### Phase 1 · Real tables, defined at runtime Build: Create physical tables and columns from user definitions, mapping each user type to a database type. Keep physical names separate from display names so renaming is free. Enforce types at the database rather than in the application. Trap: Storing every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. Done when: A user-created table exists as a real table with typed columns. Renaming a column does not rewrite data, since the physical name is stable. Number and date sorting is correct rather than lexicographic. A type constraint is enforced by the database, verified by attempting a bad insert directly. Do not build yet: No migrations, formulas, views or sharing. ### Phase 2 · Changing a column that already has data Build: Implement column type changes as migrations that first report how many rows would fail or lose information. Require confirmation. Keep the change reversible for a period by retaining the previous values. Trap: Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. Done when: A type change reports how many rows would fail or lose data before applying, tested on a column with mixed content. The change requires confirmation. The previous values are retained and the change is reversible for a stated period. A migration on a large table does not lock it for an unacceptable time. Do not build yet: No formulas, views or sharing yet. ### Phase 3 · Formulas, which are user-supplied code Build: Parse formulas into an expression tree rather than evaluating strings. Evaluate in a sandbox with no access to the environment, the filesystem or other tables beyond declared links. Detect dependency cycles at save time. Trap: Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. Done when: Formulas are parsed rather than evaluated as strings, verified by inspecting the implementation. A formula attempting to reach the environment fails at parse time, tested with a crafted expression. A dependency cycle is detected at save rather than at evaluation. A formula over ten thousand rows computes within an acceptable time. Do not build yet: No views or sharing yet. ### Phase 4 · Views that hide what they claim to hide Build: Views with filters, sorts, hidden fields and row-level rules, all applied server side. Verify that a hidden column never appears in an API response for that view, including in sorts, filters and error messages. Trap: Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. Done when: A hidden column is absent from every API response for that view, asserted in the suite across list, single-row and error responses. Row-level rules are applied in the query rather than after fetching. Sorting or filtering by a hidden column does not leak its values. A view cannot be edited by someone with only view access. Do not build yet: No sharing or realtime yet. ### Phase 5 · Sharing, and live updates that respect permissions Build: Share links with an unguessable token, a scope and revocation. Broadcast changes filtered per recipient's view permissions. Handle a row that becomes invisible to someone because its values changed. Trap: Broadcasting a change to everyone watching a table and letting each client filter it delivers rows the recipient is not permitted to see, and the filtering happens after the data has already arrived on their machine. Filter per recipient before sending, including working out that an edit made a row newly invisible to them. Done when: A live change is filtered per recipient before sending, verified by watching the traffic for a restricted user. A row edited so that it no longer matches a viewer's rules produces a removal rather than an update carrying the new values. A share token is unguessable, scoped and revocable immediately. Revoking a token disconnects active viewers. Do not build yet: Nothing after this. ### Out of scope (and why) - Automations and integrations, which is where these products make most of their money and is a whole workflow engine. - Importing from and syncing with external databases, which is a connector per system and never finished. - Scale beyond a few million rows per table, where the generic query patterns stop being adequate. - A formula language matching the breadth of a spreadsheet, which is decades of accumulated functions. ### README must contain - That user tables are real tables with real types, and why a generic value store fails later - That a column type change is a migration that previews its data loss and stays reversible - That formulas are parsed into an expression tree and never evaluated as strings - That hidden columns are absent from responses rather than hidden in the client - That live updates are filtered per recipient before being sent ===== AGENTS.md ===== # Agent instructions · NocoDB Cloud indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Real tables in PostgreSQL, one per user table, User-driven migrations with a preview and a reversible path, A parsed expression language, evaluated in a sandbox, Filters, sorts and field visibility as saved objects with their own permissions, Change broadcast per table with per-row permission filtering. 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 every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. - Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. - Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. - Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. ===== BUILD_PLAN.md ===== # Build plan · NocoDB Cloud A no-code database where the difficulty is that users design the schema at runtime: every column type change is a migration on live data, every formula is code someone typed into a cell, and every shared view is a permission decision that has to hold at the row level. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Real tables, defined at runtime A user-created table is a real table with real types. ### Steps 1. Map user types to real database types Not text for everything. The cost of that arrives after there is data. Files: `src/schema.ts` 2. Separate physical from display names So renaming is a metadata change rather than a migration. Files: `src/columns.ts` 3. Enforce types in the database Application validation is bypassed by every other code path. Files: `src/schema.ts` ### Done when - [ ] User tables are real tables with typed columns - [ ] Renaming does not rewrite data - [ ] Numbers and dates sort correctly - [ ] Type constraints are enforced by the database ### Watch out - Storing every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. ## Phase 2 · Changing a column that already has data A type change previews its damage before doing it. ### Steps 1. Preview failures and information loss Counts and examples, before anything changes. Files: `src/migrate.ts` 2. Retain previous values For a period. It is what makes undo real rather than aspirational. Files: `src/migrate.ts` 3. Avoid long locks A migration that locks a table for a minute is an outage. Files: `src/migrate.ts` ### Done when - [ ] A type change previews failures and loss - [ ] The change requires confirmation - [ ] Previous values are retained and reversible - [ ] A large table migrates without a long lock ### Watch out - Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. ## Phase 3 · Formulas, which are user-supplied code A formula computes correctly and cannot reach anything it should not. ### Steps 1. Parse to a restricted expression tree Never evaluate a string. This is the compromise vector for the category. Files: `src/formula/parse.ts`, `src/formula/parse.test.ts` 2. Fix the function set An allow list. Anything outside it fails at parse time. Files: `src/formula/functions.ts` 3. Detect cycles at save Not at evaluation, where it becomes a hang rather than an error. Files: `src/formula/cycles.ts` ### Done when - [ ] Formulas are parsed, never string-evaluated - [ ] A crafted escape attempt fails at parse time - [ ] Dependency cycles are detected at save - [ ] Ten thousand rows compute acceptably ### Watch out - Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. ## Phase 4 · Views that hide what they claim to hide A hidden column is absent from the response, not hidden in the client. ### Steps 1. Apply visibility in the query Not in the serialiser and certainly not in the client. Files: `src/views.ts` 2. Apply row rules in the query too Fetching then filtering means the rows were fetched. Files: `src/permissions.ts` 3. Assert absence across response shapes List, single row, errors. Errors are where fields leak. Files: `tests/hidden-fields.test.ts` ### Done when - [ ] Hidden columns are absent from every response shape - [ ] Row rules are applied in the query - [ ] Sorting by a hidden column leaks nothing - [ ] View-only access cannot edit the view ### Watch out - Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. ## Not in this build - Automations and workflow integrations. - External database sync connectors. - Scale beyond a few million rows per table. - A spreadsheet's full formula library. ## After v1, if you want it - Linked records between tables, which is the feature everyone wants next and complicates every query - Undo across a session rather than per migration - An audit log per row, once the permission model is provably correct ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Metadata and the user tables themselves. DATABASE_URL=postgres://localhost:5432/nocode # Required. How long previous values are kept after a type change. The undo window. MIGRATION_REVERSIBLE_DAYS=14 # Required. Present as a constant so the answer is visible. Parsed trees only. FORMULA_EVAL_STRINGS=never # Required. A formula that will not finish must not hold a request. FORMULA_TIMEOUT_MS=500 # Required. Share links are bearer credentials. Unguessable, hashed at rest. SHARE_TOKEN_BYTES=32 # Required. Where the generic query patterns stop being adequate, made explicit rather than discovered. MAX_ROWS_PER_TABLE=1000000
You are building a production product version of NocoDB Cloud. 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 ===== # NocoDB Cloud · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For NocoDB Cloud, use a managed spreadsheet interface over structured data. The hard boundary is managed hosting, integrations, collaboration, storage, and support, plus data model flexibility, collaboration, and integrations. ## Product outcome A spreadsheet interface where the data is in real typed columns, a schema change shows what it will destroy before doing it, formulas cannot escape their sandbox, and a hidden field is genuinely absent from the response. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - self-hosted server - optional object storage for attachments ## Explicit non-goals for v1 - Automations and workflow integrations. - External database sync connectors. - Scale beyond a few million rows per table. - A spreadsheet's full formula library. - managed hosting, integrations, collaboration, storage, and support - real-time multiplayer - automation and integration breadth - scale and formula depth - enterprise permissions ## Success criteria - User tables are confirmed to be real typed tables - A type change is confirmed to preview data loss - A crafted formula escape is confirmed to fail at parse time - Hidden columns are confirmed absent from every response shape - Live updates are confirmed filtered before sending ===== BRIEF.md ===== # Build brief · NocoDB Cloud 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 spreadsheet interface over a real database, where users define the schema to replace NocoDB Cloud. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Storage: Real tables in PostgreSQL, one per user table · a key-value store makes every query slow and every constraint impossible - Schema: User-driven migrations with a preview and a reversible path · a type change on live data is a migration, whatever the interface calls it - Formulas: A parsed expression language, evaluated in a sandbox · a formula is user-supplied code and must not be able to reach anything - Views: Filters, sorts and field visibility as saved objects with their own permissions · a shared view is the sharing mechanism and must not leak hidden fields - Realtime: Change broadcast per table with per-row permission filtering · a live update must not deliver a row the recipient cannot see ### Data model (create this before Phase 1) - bases(id, name, owner_id, created_at) - tables(id, base_id, name, physical_name) - columns(id, table_id, name, kind, options_json, physical_name, ordinal) · kind includes text, number, date, select, link and formula - migrations(id, table_id, from_json, to_json, rows_affected, applied_at, reversible_until) - formulas(column_id, expression, compiled_json, depends_on_column_ids) · dependencies, so a cycle is detectable - views(id, table_id, kind, filters_json, sorts_json, hidden_column_ids, share_token_hash, share_scope) - row_permissions(view_id, rule_json) · row-level rules, evaluated server side and never in the client - A migration records what it changed and remains reversible for a period, because a column type change on real data is not something to do optimistically. ### Phase 1 · Real tables, defined at runtime Build: Create physical tables and columns from user definitions, mapping each user type to a database type. Keep physical names separate from display names so renaming is free. Enforce types at the database rather than in the application. Trap: Storing every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. Done when: A user-created table exists as a real table with typed columns. Renaming a column does not rewrite data, since the physical name is stable. Number and date sorting is correct rather than lexicographic. A type constraint is enforced by the database, verified by attempting a bad insert directly. Do not build yet: No migrations, formulas, views or sharing. ### Phase 2 · Changing a column that already has data Build: Implement column type changes as migrations that first report how many rows would fail or lose information. Require confirmation. Keep the change reversible for a period by retaining the previous values. Trap: Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. Done when: A type change reports how many rows would fail or lose data before applying, tested on a column with mixed content. The change requires confirmation. The previous values are retained and the change is reversible for a stated period. A migration on a large table does not lock it for an unacceptable time. Do not build yet: No formulas, views or sharing yet. ### Phase 3 · Formulas, which are user-supplied code Build: Parse formulas into an expression tree rather than evaluating strings. Evaluate in a sandbox with no access to the environment, the filesystem or other tables beyond declared links. Detect dependency cycles at save time. Trap: Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. Done when: Formulas are parsed rather than evaluated as strings, verified by inspecting the implementation. A formula attempting to reach the environment fails at parse time, tested with a crafted expression. A dependency cycle is detected at save rather than at evaluation. A formula over ten thousand rows computes within an acceptable time. Do not build yet: No views or sharing yet. ### Phase 4 · Views that hide what they claim to hide Build: Views with filters, sorts, hidden fields and row-level rules, all applied server side. Verify that a hidden column never appears in an API response for that view, including in sorts, filters and error messages. Trap: Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. Done when: A hidden column is absent from every API response for that view, asserted in the suite across list, single-row and error responses. Row-level rules are applied in the query rather than after fetching. Sorting or filtering by a hidden column does not leak its values. A view cannot be edited by someone with only view access. Do not build yet: No sharing or realtime yet. ### Phase 5 · Sharing, and live updates that respect permissions Build: Share links with an unguessable token, a scope and revocation. Broadcast changes filtered per recipient's view permissions. Handle a row that becomes invisible to someone because its values changed. Trap: Broadcasting a change to everyone watching a table and letting each client filter it delivers rows the recipient is not permitted to see, and the filtering happens after the data has already arrived on their machine. Filter per recipient before sending, including working out that an edit made a row newly invisible to them. Done when: A live change is filtered per recipient before sending, verified by watching the traffic for a restricted user. A row edited so that it no longer matches a viewer's rules produces a removal rather than an update carrying the new values. A share token is unguessable, scoped and revocable immediately. Revoking a token disconnects active viewers. Do not build yet: Nothing after this. ### Out of scope (and why) - Automations and integrations, which is where these products make most of their money and is a whole workflow engine. - Importing from and syncing with external databases, which is a connector per system and never finished. - Scale beyond a few million rows per table, where the generic query patterns stop being adequate. - A formula language matching the breadth of a spreadsheet, which is decades of accumulated functions. ### README must contain - That user tables are real tables with real types, and why a generic value store fails later - That a column type change is a migration that previews its data loss and stays reversible - That formulas are parsed into an expression tree and never evaluated as strings - That hidden columns are absent from responses rather than hidden in the client - That live updates are filtered per recipient before being sent ===== ARCHITECTURE.md ===== # Architecture · NocoDB Cloud ## Stack | Part | Choice | Why | | --- | --- | --- | | Storage | Real tables in PostgreSQL, one per user table | a key-value store makes every query slow and every constraint impossible | | Schema | User-driven migrations with a preview and a reversible path | a type change on live data is a migration, whatever the interface calls it | | Formulas | A parsed expression language, evaluated in a sandbox | a formula is user-supplied code and must not be able to reach anything | | Views | Filters, sorts and field visibility as saved objects with their own permissions | a shared view is the sharing mechanism and must not leak hidden fields | | Realtime | Change broadcast per table with per-row permission filtering | a live update must not deliver a row the recipient cannot see | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Schema | Real tables from user definitions. | Not swappable for a generic value store, whose cost arrives after there is data to migrate. | | Migrations | Previewed, reversible type changes. | Any implementation, provided the preview precedes the change. | | Formulas | Parsing and sandboxed evaluation. | Any parser, provided strings are never evaluated. | | Views | Server-side visibility and row rules. | Any implementation, provided filtering happens in the query. | | Realtime | Per-recipient filtered broadcast. | Any transport, provided filtering precedes sending. | ## 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 · Metadata and the user tables themselves. - `MIGRATION_REVERSIBLE_DAYS` · required · How long previous values are kept after a type change. The undo window. - `FORMULA_EVAL_STRINGS` · required · Present as a constant so the answer is visible. Parsed trees only. - `FORMULA_TIMEOUT_MS` · required · A formula that will not finish must not hold a request. - `SHARE_TOKEN_BYTES` · required · Share links are bearer credentials. Unguessable, hashed at rest. - `MAX_ROWS_PER_TABLE` · required · Where the generic query patterns stop being adequate, made explicit rather than discovered. ## 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 · NocoDB Cloud product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Real tables in PostgreSQL, one per user table, User-driven migrations with a preview and a reversible path, A parsed expression language, evaluated in a sandbox, Filters, sorts and field visibility as saved objects with their own permissions, Change broadcast per table with per-row permission filtering. - 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 every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. - Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. - Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. - Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. - Broadcasting a change to everyone watching a table and letting each client filter it delivers rows the recipient is not permitted to see, and the filtering happens after the data has already arrived on their machine. Filter per recipient before sending, including working out that an edit made a row newly invisible to them. ===== MILESTONES.md ===== # Delivery milestones · NocoDB Cloud Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Real tables, defined at runtime A user-created table is a real table with real types. ### Steps 1. Map user types to real database types Not text for everything. The cost of that arrives after there is data. Files: `src/schema.ts` 2. Separate physical from display names So renaming is a metadata change rather than a migration. Files: `src/columns.ts` 3. Enforce types in the database Application validation is bypassed by every other code path. Files: `src/schema.ts` ### Done when - [ ] User tables are real tables with typed columns - [ ] Renaming does not rewrite data - [ ] Numbers and dates sort correctly - [ ] Type constraints are enforced by the database ### Watch out - Storing every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. ## M2 · Changing a column that already has data A type change previews its damage before doing it. ### Steps 1. Preview failures and information loss Counts and examples, before anything changes. Files: `src/migrate.ts` 2. Retain previous values For a period. It is what makes undo real rather than aspirational. Files: `src/migrate.ts` 3. Avoid long locks A migration that locks a table for a minute is an outage. Files: `src/migrate.ts` ### Done when - [ ] A type change previews failures and loss - [ ] The change requires confirmation - [ ] Previous values are retained and reversible - [ ] A large table migrates without a long lock ### Watch out - Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. ## M3 · Formulas, which are user-supplied code A formula computes correctly and cannot reach anything it should not. ### Steps 1. Parse to a restricted expression tree Never evaluate a string. This is the compromise vector for the category. Files: `src/formula/parse.ts`, `src/formula/parse.test.ts` 2. Fix the function set An allow list. Anything outside it fails at parse time. Files: `src/formula/functions.ts` 3. Detect cycles at save Not at evaluation, where it becomes a hang rather than an error. Files: `src/formula/cycles.ts` ### Done when - [ ] Formulas are parsed, never string-evaluated - [ ] A crafted escape attempt fails at parse time - [ ] Dependency cycles are detected at save - [ ] Ten thousand rows compute acceptably ### Watch out - Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. ## M4 · Views that hide what they claim to hide A hidden column is absent from the response, not hidden in the client. ### Steps 1. Apply visibility in the query Not in the serialiser and certainly not in the client. Files: `src/views.ts` 2. Apply row rules in the query too Fetching then filtering means the rows were fetched. Files: `src/permissions.ts` 3. Assert absence across response shapes List, single row, errors. Errors are where fields leak. Files: `tests/hidden-fields.test.ts` ### Done when - [ ] Hidden columns are absent from every response shape - [ ] Row rules are applied in the query - [ ] Sorting by a hidden column leaks nothing - [ ] View-only access cannot edit the view ### Watch out - Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. ## M5 · Sharing, and live updates that respect permissions (production only) A shared link shows exactly what it should, and live changes do not leak. ### Steps 1. Filter broadcasts per recipient Before sending. Client-side filtering means it already arrived. Files: `src/realtime.ts` 2. Handle rows becoming invisible An edit can move a row out of someone's permission. Send a removal. Files: `src/realtime.ts`, `tests/realtime-leak.test.ts` 3. Make share tokens revocable immediately Including for viewers currently connected. Files: `src/share.ts` ### Done when - [ ] Live changes are filtered per recipient before sending - [ ] A newly invisible row produces a removal, not an update - [ ] Share tokens are unguessable and scoped - [ ] Revocation disconnects active viewers ### Watch out - Broadcasting a change to everyone watching a table and letting each client filter it delivers rows the recipient is not permitted to see, and the filtering happens after the data has already arrived on their machine. Filter per recipient before sending, including working out that an edit made a row newly invisible to them. ===== OPERATIONS.md ===== # Operations · NocoDB Cloud ## Backup User data and the schema metadata must be backed up together, since a restored table without its column definitions is unreadable and column definitions without their tables are meaningless. ## Restore Restore and confirm schema metadata matches the physical tables, since a mismatch presents users with columns that do not exist or hides data that does. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Migration duration and formula evaluation time. A migration that is slow on a small table will be an outage on a large one, and that is visible before it happens. ## Incident checklist A hidden field appearing in a response is the failure that matters, because shared views make it visible to people outside the organisation. The suite assertion covers list, single-row and error shapes, and error shapes are where it usually reappears. 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 - [ ] User tables are confirmed to be real typed tables - [ ] A type change is confirmed to preview data loss - [ ] A crafted formula escape is confirmed to fail at parse time - [ ] Hidden columns are confirmed absent from every response shape - [ ] Live updates are confirmed filtered before sending ## Launch constraint Do not market omitted NocoDB Cloud 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. Metadata and the user tables themselves. DATABASE_URL=postgres://localhost:5432/nocode # Required. How long previous values are kept after a type change. The undo window. MIGRATION_REVERSIBLE_DAYS=14 # Required. Present as a constant so the answer is visible. Parsed trees only. FORMULA_EVAL_STRINGS=never # Required. A formula that will not finish must not hold a request. FORMULA_TIMEOUT_MS=500 # Required. Share links are bearer credentials. Unguessable, hashed at rest. SHARE_TOKEN_BYTES=32 # Required. Where the generic query patterns stop being adequate, made explicit rather than discovered. MAX_ROWS_PER_TABLE=1000000
# NocoDB Cloud · indie build A no-code database where the difficulty is that users design the schema at runtime: every column type change is a migration on live data, every formula is code someone typed into a cell, and every shared view is a permission decision that has to hold at the row level. 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 | | --- | --- | --- | | Storage | Real tables in PostgreSQL, one per user table | a key-value store makes every query slow and every constraint impossible | | Schema | User-driven migrations with a preview and a reversible path | a type change on live data is a migration, whatever the interface calls it | | Formulas | A parsed expression language, evaluated in a sandbox | a formula is user-supplied code and must not be able to reach anything | | Views | Filters, sorts and field visibility as saved objects with their own permissions | a shared view is the sharing mechanism and must not leak hidden fields | | Realtime | Change broadcast per table with per-row permission filtering | a live update must not deliver a row the recipient cannot see | ## 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, the formula parser and the realtime layer. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: User tables are real tables, which needs a database that handles runtime schema changes without long locks. - Get it: Docker or your package manager. - Verify: A column type change on a large table completes without an unacceptable lock - [ ] **A parser generator or hand-written parser plan** · free - Why: The formula language is the security boundary, and reaching for the language's own evaluator is how this category gets compromised. - Get it: Decide the grammar and the function allow list before writing the parser. - Verify: A written grammar exists and the function list is finite - [ ] **A real table with messy data** · free - Why: Migrations, formulas and sorting all behave differently against real data, which contains a note in a number column and a date nobody can parse. - Get it: Import a real spreadsheet you use, including the columns that have inconsistent values. - Verify: At least one column has mixed types and one has values that fail to parse - [ ] **Your sharing model, decided early** · free - Why: Row-level rules and hidden fields have to be enforced in the query, and retrofitting that after building on client-side filtering is a rewrite of the data access layer. - Get it: Decide whether row rules exist, what a share link may expose, and whether shared views can be edited. Write it down before phase 4. - Verify: A written note states the rules and what a share token grants ## 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: - Automations and workflow integrations. - External database sync connectors. - Scale beyond a few million rows per table. - A spreadsheet's full formula library. - managed hosting, integrations, collaboration, storage, and support - real-time multiplayer - automation and integration breadth - scale and formula depth - enterprise permissions If one of those is essential to you, that is the reason to keep paying for NocoDB Cloud, and the README should say so rather than pretend.
# Build brief · NocoDB Cloud 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 spreadsheet interface over a real database, where users define the schema to replace NocoDB Cloud. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Storage: Real tables in PostgreSQL, one per user table · a key-value store makes every query slow and every constraint impossible - Schema: User-driven migrations with a preview and a reversible path · a type change on live data is a migration, whatever the interface calls it - Formulas: A parsed expression language, evaluated in a sandbox · a formula is user-supplied code and must not be able to reach anything - Views: Filters, sorts and field visibility as saved objects with their own permissions · a shared view is the sharing mechanism and must not leak hidden fields - Realtime: Change broadcast per table with per-row permission filtering · a live update must not deliver a row the recipient cannot see ### Data model (create this before Phase 1) - bases(id, name, owner_id, created_at) - tables(id, base_id, name, physical_name) - columns(id, table_id, name, kind, options_json, physical_name, ordinal) · kind includes text, number, date, select, link and formula - migrations(id, table_id, from_json, to_json, rows_affected, applied_at, reversible_until) - formulas(column_id, expression, compiled_json, depends_on_column_ids) · dependencies, so a cycle is detectable - views(id, table_id, kind, filters_json, sorts_json, hidden_column_ids, share_token_hash, share_scope) - row_permissions(view_id, rule_json) · row-level rules, evaluated server side and never in the client - A migration records what it changed and remains reversible for a period, because a column type change on real data is not something to do optimistically. ### Phase 1 · Real tables, defined at runtime Build: Create physical tables and columns from user definitions, mapping each user type to a database type. Keep physical names separate from display names so renaming is free. Enforce types at the database rather than in the application. Trap: Storing every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. Done when: A user-created table exists as a real table with typed columns. Renaming a column does not rewrite data, since the physical name is stable. Number and date sorting is correct rather than lexicographic. A type constraint is enforced by the database, verified by attempting a bad insert directly. Do not build yet: No migrations, formulas, views or sharing. ### Phase 2 · Changing a column that already has data Build: Implement column type changes as migrations that first report how many rows would fail or lose information. Require confirmation. Keep the change reversible for a period by retaining the previous values. Trap: Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. Done when: A type change reports how many rows would fail or lose data before applying, tested on a column with mixed content. The change requires confirmation. The previous values are retained and the change is reversible for a stated period. A migration on a large table does not lock it for an unacceptable time. Do not build yet: No formulas, views or sharing yet. ### Phase 3 · Formulas, which are user-supplied code Build: Parse formulas into an expression tree rather than evaluating strings. Evaluate in a sandbox with no access to the environment, the filesystem or other tables beyond declared links. Detect dependency cycles at save time. Trap: Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. Done when: Formulas are parsed rather than evaluated as strings, verified by inspecting the implementation. A formula attempting to reach the environment fails at parse time, tested with a crafted expression. A dependency cycle is detected at save rather than at evaluation. A formula over ten thousand rows computes within an acceptable time. Do not build yet: No views or sharing yet. ### Phase 4 · Views that hide what they claim to hide Build: Views with filters, sorts, hidden fields and row-level rules, all applied server side. Verify that a hidden column never appears in an API response for that view, including in sorts, filters and error messages. Trap: Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. Done when: A hidden column is absent from every API response for that view, asserted in the suite across list, single-row and error responses. Row-level rules are applied in the query rather than after fetching. Sorting or filtering by a hidden column does not leak its values. A view cannot be edited by someone with only view access. Do not build yet: No sharing or realtime yet. ### Phase 5 · Sharing, and live updates that respect permissions Build: Share links with an unguessable token, a scope and revocation. Broadcast changes filtered per recipient's view permissions. Handle a row that becomes invisible to someone because its values changed. Trap: Broadcasting a change to everyone watching a table and letting each client filter it delivers rows the recipient is not permitted to see, and the filtering happens after the data has already arrived on their machine. Filter per recipient before sending, including working out that an edit made a row newly invisible to them. Done when: A live change is filtered per recipient before sending, verified by watching the traffic for a restricted user. A row edited so that it no longer matches a viewer's rules produces a removal rather than an update carrying the new values. A share token is unguessable, scoped and revocable immediately. Revoking a token disconnects active viewers. Do not build yet: Nothing after this. ### Out of scope (and why) - Automations and integrations, which is where these products make most of their money and is a whole workflow engine. - Importing from and syncing with external databases, which is a connector per system and never finished. - Scale beyond a few million rows per table, where the generic query patterns stop being adequate. - A formula language matching the breadth of a spreadsheet, which is decades of accumulated functions. ### README must contain - That user tables are real tables with real types, and why a generic value store fails later - That a column type change is a migration that previews its data loss and stays reversible - That formulas are parsed into an expression tree and never evaluated as strings - That hidden columns are absent from responses rather than hidden in the client - That live updates are filtered per recipient before being sent
# Agent instructions · NocoDB Cloud indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Real tables in PostgreSQL, one per user table, User-driven migrations with a preview and a reversible path, A parsed expression language, evaluated in a sandbox, Filters, sorts and field visibility as saved objects with their own permissions, Change broadcast per table with per-row permission filtering. 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 every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. - Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. - Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. - Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape.
# Build plan · NocoDB Cloud A no-code database where the difficulty is that users design the schema at runtime: every column type change is a migration on live data, every formula is code someone typed into a cell, and every shared view is a permission decision that has to hold at the row level. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Real tables, defined at runtime A user-created table is a real table with real types. ### Steps 1. Map user types to real database types Not text for everything. The cost of that arrives after there is data. Files: `src/schema.ts` 2. Separate physical from display names So renaming is a metadata change rather than a migration. Files: `src/columns.ts` 3. Enforce types in the database Application validation is bypassed by every other code path. Files: `src/schema.ts` ### Done when - [ ] User tables are real tables with typed columns - [ ] Renaming does not rewrite data - [ ] Numbers and dates sort correctly - [ ] Type constraints are enforced by the database ### Watch out - Storing every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. ## Phase 2 · Changing a column that already has data A type change previews its damage before doing it. ### Steps 1. Preview failures and information loss Counts and examples, before anything changes. Files: `src/migrate.ts` 2. Retain previous values For a period. It is what makes undo real rather than aspirational. Files: `src/migrate.ts` 3. Avoid long locks A migration that locks a table for a minute is an outage. Files: `src/migrate.ts` ### Done when - [ ] A type change previews failures and loss - [ ] The change requires confirmation - [ ] Previous values are retained and reversible - [ ] A large table migrates without a long lock ### Watch out - Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. ## Phase 3 · Formulas, which are user-supplied code A formula computes correctly and cannot reach anything it should not. ### Steps 1. Parse to a restricted expression tree Never evaluate a string. This is the compromise vector for the category. Files: `src/formula/parse.ts`, `src/formula/parse.test.ts` 2. Fix the function set An allow list. Anything outside it fails at parse time. Files: `src/formula/functions.ts` 3. Detect cycles at save Not at evaluation, where it becomes a hang rather than an error. Files: `src/formula/cycles.ts` ### Done when - [ ] Formulas are parsed, never string-evaluated - [ ] A crafted escape attempt fails at parse time - [ ] Dependency cycles are detected at save - [ ] Ten thousand rows compute acceptably ### Watch out - Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. ## Phase 4 · Views that hide what they claim to hide A hidden column is absent from the response, not hidden in the client. ### Steps 1. Apply visibility in the query Not in the serialiser and certainly not in the client. Files: `src/views.ts` 2. Apply row rules in the query too Fetching then filtering means the rows were fetched. Files: `src/permissions.ts` 3. Assert absence across response shapes List, single row, errors. Errors are where fields leak. Files: `tests/hidden-fields.test.ts` ### Done when - [ ] Hidden columns are absent from every response shape - [ ] Row rules are applied in the query - [ ] Sorting by a hidden column leaks nothing - [ ] View-only access cannot edit the view ### Watch out - Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. ## Not in this build - Automations and workflow integrations. - External database sync connectors. - Scale beyond a few million rows per table. - A spreadsheet's full formula library. ## After v1, if you want it - Linked records between tables, which is the feature everyone wants next and complicates every query - Undo across a session rather than per migration - An audit log per row, once the permission model is provably correct
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Metadata and the user tables themselves. DATABASE_URL=postgres://localhost:5432/nocode # Required. How long previous values are kept after a type change. The undo window. MIGRATION_REVERSIBLE_DAYS=14 # Required. Present as a constant so the answer is visible. Parsed trees only. FORMULA_EVAL_STRINGS=never # Required. A formula that will not finish must not hold a request. FORMULA_TIMEOUT_MS=500 # Required. Share links are bearer credentials. Unguessable, hashed at rest. SHARE_TOKEN_BYTES=32 # Required. Where the generic query patterns stop being adequate, made explicit rather than discovered. MAX_ROWS_PER_TABLE=1000000
# NocoDB Cloud · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For NocoDB Cloud, use a managed spreadsheet interface over structured data. The hard boundary is managed hosting, integrations, collaboration, storage, and support, plus data model flexibility, collaboration, and integrations. ## Product outcome A spreadsheet interface where the data is in real typed columns, a schema change shows what it will destroy before doing it, formulas cannot escape their sandbox, and a hidden field is genuinely absent from the response. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - self-hosted server - optional object storage for attachments ## Explicit non-goals for v1 - Automations and workflow integrations. - External database sync connectors. - Scale beyond a few million rows per table. - A spreadsheet's full formula library. - managed hosting, integrations, collaboration, storage, and support - real-time multiplayer - automation and integration breadth - scale and formula depth - enterprise permissions ## Success criteria - User tables are confirmed to be real typed tables - A type change is confirmed to preview data loss - A crafted formula escape is confirmed to fail at parse time - Hidden columns are confirmed absent from every response shape - Live updates are confirmed filtered before sending
# Build brief · NocoDB Cloud 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 spreadsheet interface over a real database, where users define the schema to replace NocoDB Cloud. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Storage: Real tables in PostgreSQL, one per user table · a key-value store makes every query slow and every constraint impossible - Schema: User-driven migrations with a preview and a reversible path · a type change on live data is a migration, whatever the interface calls it - Formulas: A parsed expression language, evaluated in a sandbox · a formula is user-supplied code and must not be able to reach anything - Views: Filters, sorts and field visibility as saved objects with their own permissions · a shared view is the sharing mechanism and must not leak hidden fields - Realtime: Change broadcast per table with per-row permission filtering · a live update must not deliver a row the recipient cannot see ### Data model (create this before Phase 1) - bases(id, name, owner_id, created_at) - tables(id, base_id, name, physical_name) - columns(id, table_id, name, kind, options_json, physical_name, ordinal) · kind includes text, number, date, select, link and formula - migrations(id, table_id, from_json, to_json, rows_affected, applied_at, reversible_until) - formulas(column_id, expression, compiled_json, depends_on_column_ids) · dependencies, so a cycle is detectable - views(id, table_id, kind, filters_json, sorts_json, hidden_column_ids, share_token_hash, share_scope) - row_permissions(view_id, rule_json) · row-level rules, evaluated server side and never in the client - A migration records what it changed and remains reversible for a period, because a column type change on real data is not something to do optimistically. ### Phase 1 · Real tables, defined at runtime Build: Create physical tables and columns from user definitions, mapping each user type to a database type. Keep physical names separate from display names so renaming is free. Enforce types at the database rather than in the application. Trap: Storing every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. Done when: A user-created table exists as a real table with typed columns. Renaming a column does not rewrite data, since the physical name is stable. Number and date sorting is correct rather than lexicographic. A type constraint is enforced by the database, verified by attempting a bad insert directly. Do not build yet: No migrations, formulas, views or sharing. ### Phase 2 · Changing a column that already has data Build: Implement column type changes as migrations that first report how many rows would fail or lose information. Require confirmation. Keep the change reversible for a period by retaining the previous values. Trap: Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. Done when: A type change reports how many rows would fail or lose data before applying, tested on a column with mixed content. The change requires confirmation. The previous values are retained and the change is reversible for a stated period. A migration on a large table does not lock it for an unacceptable time. Do not build yet: No formulas, views or sharing yet. ### Phase 3 · Formulas, which are user-supplied code Build: Parse formulas into an expression tree rather than evaluating strings. Evaluate in a sandbox with no access to the environment, the filesystem or other tables beyond declared links. Detect dependency cycles at save time. Trap: Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. Done when: Formulas are parsed rather than evaluated as strings, verified by inspecting the implementation. A formula attempting to reach the environment fails at parse time, tested with a crafted expression. A dependency cycle is detected at save rather than at evaluation. A formula over ten thousand rows computes within an acceptable time. Do not build yet: No views or sharing yet. ### Phase 4 · Views that hide what they claim to hide Build: Views with filters, sorts, hidden fields and row-level rules, all applied server side. Verify that a hidden column never appears in an API response for that view, including in sorts, filters and error messages. Trap: Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. Done when: A hidden column is absent from every API response for that view, asserted in the suite across list, single-row and error responses. Row-level rules are applied in the query rather than after fetching. Sorting or filtering by a hidden column does not leak its values. A view cannot be edited by someone with only view access. Do not build yet: No sharing or realtime yet. ### Phase 5 · Sharing, and live updates that respect permissions Build: Share links with an unguessable token, a scope and revocation. Broadcast changes filtered per recipient's view permissions. Handle a row that becomes invisible to someone because its values changed. Trap: Broadcasting a change to everyone watching a table and letting each client filter it delivers rows the recipient is not permitted to see, and the filtering happens after the data has already arrived on their machine. Filter per recipient before sending, including working out that an edit made a row newly invisible to them. Done when: A live change is filtered per recipient before sending, verified by watching the traffic for a restricted user. A row edited so that it no longer matches a viewer's rules produces a removal rather than an update carrying the new values. A share token is unguessable, scoped and revocable immediately. Revoking a token disconnects active viewers. Do not build yet: Nothing after this. ### Out of scope (and why) - Automations and integrations, which is where these products make most of their money and is a whole workflow engine. - Importing from and syncing with external databases, which is a connector per system and never finished. - Scale beyond a few million rows per table, where the generic query patterns stop being adequate. - A formula language matching the breadth of a spreadsheet, which is decades of accumulated functions. ### README must contain - That user tables are real tables with real types, and why a generic value store fails later - That a column type change is a migration that previews its data loss and stays reversible - That formulas are parsed into an expression tree and never evaluated as strings - That hidden columns are absent from responses rather than hidden in the client - That live updates are filtered per recipient before being sent
# Architecture · NocoDB Cloud ## Stack | Part | Choice | Why | | --- | --- | --- | | Storage | Real tables in PostgreSQL, one per user table | a key-value store makes every query slow and every constraint impossible | | Schema | User-driven migrations with a preview and a reversible path | a type change on live data is a migration, whatever the interface calls it | | Formulas | A parsed expression language, evaluated in a sandbox | a formula is user-supplied code and must not be able to reach anything | | Views | Filters, sorts and field visibility as saved objects with their own permissions | a shared view is the sharing mechanism and must not leak hidden fields | | Realtime | Change broadcast per table with per-row permission filtering | a live update must not deliver a row the recipient cannot see | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Schema | Real tables from user definitions. | Not swappable for a generic value store, whose cost arrives after there is data to migrate. | | Migrations | Previewed, reversible type changes. | Any implementation, provided the preview precedes the change. | | Formulas | Parsing and sandboxed evaluation. | Any parser, provided strings are never evaluated. | | Views | Server-side visibility and row rules. | Any implementation, provided filtering happens in the query. | | Realtime | Per-recipient filtered broadcast. | Any transport, provided filtering precedes sending. | ## 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 · Metadata and the user tables themselves. - `MIGRATION_REVERSIBLE_DAYS` · required · How long previous values are kept after a type change. The undo window. - `FORMULA_EVAL_STRINGS` · required · Present as a constant so the answer is visible. Parsed trees only. - `FORMULA_TIMEOUT_MS` · required · A formula that will not finish must not hold a request. - `SHARE_TOKEN_BYTES` · required · Share links are bearer credentials. Unguessable, hashed at rest. - `MAX_ROWS_PER_TABLE` · required · Where the generic query patterns stop being adequate, made explicit rather than discovered. ## 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 · NocoDB Cloud product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Real tables in PostgreSQL, one per user table, User-driven migrations with a preview and a reversible path, A parsed expression language, evaluated in a sandbox, Filters, sorts and field visibility as saved objects with their own permissions, Change broadcast per table with per-row permission filtering. - 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 every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. - Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. - Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. - Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. - Broadcasting a change to everyone watching a table and letting each client filter it delivers rows the recipient is not permitted to see, and the filtering happens after the data has already arrived on their machine. Filter per recipient before sending, including working out that an edit made a row newly invisible to them.
# Delivery milestones · NocoDB Cloud Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Real tables, defined at runtime A user-created table is a real table with real types. ### Steps 1. Map user types to real database types Not text for everything. The cost of that arrives after there is data. Files: `src/schema.ts` 2. Separate physical from display names So renaming is a metadata change rather than a migration. Files: `src/columns.ts` 3. Enforce types in the database Application validation is bypassed by every other code path. Files: `src/schema.ts` ### Done when - [ ] User tables are real tables with typed columns - [ ] Renaming does not rewrite data - [ ] Numbers and dates sort correctly - [ ] Type constraints are enforced by the database ### Watch out - Storing every value as text in a generic key-value table makes every filter a scan, every sort wrong for numbers and dates, and every constraint impossible, and by the time that hurts there is user data to migrate. Create real columns with real types, even though it means implementing migrations. ## M2 · Changing a column that already has data A type change previews its damage before doing it. ### Steps 1. Preview failures and information loss Counts and examples, before anything changes. Files: `src/migrate.ts` 2. Retain previous values For a period. It is what makes undo real rather than aspirational. Files: `src/migrate.ts` 3. Avoid long locks A migration that locks a table for a minute is an outage. Files: `src/migrate.ts` ### Done when - [ ] A type change previews failures and loss - [ ] The change requires confirmation - [ ] Previous values are retained and reversible - [ ] A large table migrates without a long lock ### Watch out - Converting a text column to a number by dropping whatever fails to parse silently destroys the rows that had a note in them, and nobody notices until someone looks for the note. Count and show the failures first, and keep the original values so the change can be undone. ## M3 · Formulas, which are user-supplied code A formula computes correctly and cannot reach anything it should not. ### Steps 1. Parse to a restricted expression tree Never evaluate a string. This is the compromise vector for the category. Files: `src/formula/parse.ts`, `src/formula/parse.test.ts` 2. Fix the function set An allow list. Anything outside it fails at parse time. Files: `src/formula/functions.ts` 3. Detect cycles at save Not at evaluation, where it becomes a hang rather than an error. Files: `src/formula/cycles.ts` ### Done when - [ ] Formulas are parsed, never string-evaluated - [ ] A crafted escape attempt fails at parse time - [ ] Dependency cycles are detected at save - [ ] Ten thousand rows compute acceptably ### Watch out - Evaluating a formula by passing it to the language's own evaluator gives every user arbitrary code execution on your server, and it is the single most common way this category of product gets compromised. Parse to a restricted expression tree with a fixed function set, and never evaluate a string. ## M4 · Views that hide what they claim to hide A hidden column is absent from the response, not hidden in the client. ### Steps 1. Apply visibility in the query Not in the serialiser and certainly not in the client. Files: `src/views.ts` 2. Apply row rules in the query too Fetching then filtering means the rows were fetched. Files: `src/permissions.ts` 3. Assert absence across response shapes List, single row, errors. Errors are where fields leak. Files: `tests/hidden-fields.test.ts` ### Done when - [ ] Hidden columns are absent from every response shape - [ ] Row rules are applied in the query - [ ] Sorting by a hidden column leaks nothing - [ ] View-only access cannot edit the view ### Watch out - Hiding a column in the interface while the API still returns it means anyone who opens the network tab sees the salary field you thought was hidden, and shared views make that a stranger rather than a colleague. Filter fields in the query itself, and assert in the suite that hidden fields are absent from every response shape. ## M5 · Sharing, and live updates that respect permissions (production only) A shared link shows exactly what it should, and live changes do not leak. ### Steps 1. Filter broadcasts per recipient Before sending. Client-side filtering means it already arrived. Files: `src/realtime.ts` 2. Handle rows becoming invisible An edit can move a row out of someone's permission. Send a removal. Files: `src/realtime.ts`, `tests/realtime-leak.test.ts` 3. Make share tokens revocable immediately Including for viewers currently connected. Files: `src/share.ts` ### Done when - [ ] Live changes are filtered per recipient before sending - [ ] A newly invisible row produces a removal, not an update - [ ] Share tokens are unguessable and scoped - [ ] Revocation disconnects active viewers ### Watch out - Broadcasting a change to everyone watching a table and letting each client filter it delivers rows the recipient is not permitted to see, and the filtering happens after the data has already arrived on their machine. Filter per recipient before sending, including working out that an edit made a row newly invisible to them.
# Operations · NocoDB Cloud ## Backup User data and the schema metadata must be backed up together, since a restored table without its column definitions is unreadable and column definitions without their tables are meaningless. ## Restore Restore and confirm schema metadata matches the physical tables, since a mismatch presents users with columns that do not exist or hides data that does. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Migration duration and formula evaluation time. A migration that is slow on a small table will be an outage on a large one, and that is visible before it happens. ## Incident checklist A hidden field appearing in a response is the failure that matters, because shared views make it visible to people outside the organisation. The suite assertion covers list, single-row and error shapes, and error shapes are where it usually reappears. 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 - [ ] User tables are confirmed to be real typed tables - [ ] A type change is confirmed to preview data loss - [ ] A crafted formula escape is confirmed to fail at parse time - [ ] Hidden columns are confirmed absent from every response shape - [ ] Live updates are confirmed filtered before sending ## Launch constraint Do not market omitted NocoDB Cloud 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. Metadata and the user tables themselves. DATABASE_URL=postgres://localhost:5432/nocode # Required. How long previous values are kept after a type change. The undo window. MIGRATION_REVERSIBLE_DAYS=14 # Required. Present as a constant so the answer is visible. Parsed trees only. FORMULA_EVAL_STRINGS=never # Required. A formula that will not finish must not hold a request. FORMULA_TIMEOUT_MS=500 # Required. Share links are bearer credentials. Unguessable, hashed at rest. SHARE_TOKEN_BYTES=32 # Required. Where the generic query patterns stop being adequate, made explicit rather than discovered. MAX_ROWS_PER_TABLE=1000000
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for NocoDB Cloud because teams pay when a flexible database becomes a shared operational system with integrations and years of accumulated business logic. The recurring cost buys schema changes, formulas, permissions, imports, attachments, views, APIs, concurrency, backups, and migrations, not just the visible interface.
xmanaged hosting, integrations, collaboration, storage, and support
xreal-time multiplayer
xautomation and integration breadth
xscale and formula depth
xenterprise permissions
Don't feel like building it? These folks already made it free.
all 5 free alternatives to NocoDB Cloud →· no votes, no pay-to-list · just what's real
NocoDB Cloud pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | Up to 3 editors and 10 commenters; 1,000 records; 1 GB storage; 100 automation runs/month; 1,000 API calls/month; 1 extension; 0 external database connections. |
| plus | $15/user | $12/user | 50,000 records; 20 GB storage; 100,000 API calls/month; 30-day revision history; 2 snapshots; 0 external database connections. |
| business | $30/user | $24/user | 300,000 records; 100 GB storage; 1 million API calls/month; 10 external database connections; 90-day history; 5 snapshots. |
| scale | $54/user | $45/user | 1 million records; 150 GB storage; 5 million API calls/month; 180-day history; 10 snapshots. |
| enterprise | custom | — | Unlimited records; 1 TB storage; 365+ days of history; 50+ snapshots; custom usage and security limits. |
free tier3 editors, 10 commenters, 1,000 records, 1 GB storage, 100 automation runs/month, 1,000 API calls/month, 1 extension and no external database connections
billingmonthly + annual; published annual rates are 20% lower
hidden costsPlus and Business billing is capped at 9 paid seats ($135/$270 monthly or $108/$216 annual-equivalent); Scale has contradictory 0-versus-3-seat-minimum language; overage rates are not published, so higher usage may require an upgrade.
verified 2026-08-11 · source ↗
Vibecode NocoDB Cloud
Kinda. The core of NocoDB Cloud is buildable in a weekend with the prompt on this page, but there are real gaps: managed hosting, integrations, collaboration, storage, and support, real-time multiplayer. Read the honest list above before committing.
How much does NocoDB Cloud cost?
NocoDB Cloud costs about $15/month (Plus, checked 2026-07-31), which is $180 per year.
What do I lose by replacing NocoDB Cloud?
Honestly: managed hosting, integrations, collaboration, storage, and support; real-time multiplayer; automation and integration breadth; scale and formula depth; enterprise permissions. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to NocoDB Cloud?
Yes: Grist (Self-hosted rather than managed, but a polished relational grid with formulas and forms.) Baserow Free (Airtable with fewer ornaments: 3,000 rows, forms, views, automations, and a small app builder for $0.) NocoDB Free (The same managed product: three editors, 1,000 records, forms, grid, kanban, gallery, webhooks, and API access.) All 5 curated free alternatives are at vibecodeit.com/nocodb-cloud/alternatives. The prompt is for when you want it exactly your way.