Vibecode Appsmith 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 Appsmith Cloud, compose an internal dashboard from a database and a fixed widget library. The hard boundary is connectors, enterprise permissions, workflows, support, and managed hosting, plus connectors, runtime reliability, and governance.
You are building a lean indie version of Appsmith 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 ===== # Appsmith Cloud · indie build An internal tool builder where the interesting work is composing an interface from queries and widgets, and the discipline is that everything touching a production database is parameterised, scoped, confirmed and logged. 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 | | --- | --- | --- | | Data access | Parameterised queries only, stored as templates with typed parameters | string interpolation is the vulnerability this category is prone to | | Credentials | Per-datasource, scoped to the minimum the tools need | a builder with administrative access is one bug from an incident | | Widgets | A fixed library bound to query results | an unbounded widget set becomes a framework nobody can secure | | Permissions | Per-tool and per-action, enforced server side | a hidden button is not a permission | | Runtime | TypeScript with PostgreSQL for its own state | the builder's state is separate from the data it displays | ## 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 builder and the query execution layer. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: The builder's own state, and a realistic target database for the tools it builds. - Get it: Docker or your package manager, with a separate database for the builder and the target. - Verify: Two databases exist and the builder cannot reach the target with its own credentials - [ ] **Database credentials at two scope levels** · free - Why: The read-only guarantee is a database property rather than an application promise, and it has to be tested against real credentials that genuinely cannot write. - Get it: Create a read-only role and a narrowly scoped write role on your target database. - Verify: The read-only role's write attempt is refused by the database - [ ] **A realistic target database** · free - Why: Row limits, oversized results and the consequences of a mistaken parameter only become real against a table with enough rows to matter. - Get it: A copy of a real schema with at least a hundred thousand rows in the main table. - Verify: A query without a limit would return far more than a widget can display - [ ] **Your rules for mutating tools** · free - Why: The confirmation threshold and who may run mutating queries are decisions that belong before the first tool rather than after the first accident. - Get it: Decide the row count above which extra acknowledgement is required, and who may create a mutating query at all. - Verify: A written policy states both and the threshold is configured ## 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: - Breadth of datasource connectors. - Custom widget development by users. - A general application platform. - Schema migration or administration. - connectors, enterprise permissions, workflows, support, and managed hosting - hundreds of connectors - pixel-perfect app design - mobile app-store builds - enterprise permissions and scale If one of those is essential to you, that is the reason to keep paying for Appsmith Cloud, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Appsmith 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 internal tools over a real database, where a wrong click cannot destroy a table to replace Appsmith 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. ### Rule zero This connects to production data and gives people a button that changes it. Two constraints follow and both are structural. Queries are parameterised and never assembled from user input as text. A tool builder that interpolates values into a query string hands anyone who can use the tool the ability to run whatever they like against the database, and internal tools are exactly where that is least noticed because everyone using them is trusted. Second, the database connection is scoped to what the tool needs. A read-only dashboard uses read-only credentials. A tool that updates one table uses credentials that can only update that table. The alternative is a builder holding administrative access to production, which is one bug away from a very bad afternoon. The third thing, less absolute but nearly as important: destructive actions are confirmed with what they will affect, and they are logged with who did it. Internal tools accumulate buttons nobody remembers adding. ### Stack (fixed, do not substitute) - Data access: Parameterised queries only, stored as templates with typed parameters · string interpolation is the vulnerability this category is prone to - Credentials: Per-datasource, scoped to the minimum the tools need · a builder with administrative access is one bug from an incident - Widgets: A fixed library bound to query results · an unbounded widget set becomes a framework nobody can secure - Permissions: Per-tool and per-action, enforced server side · a hidden button is not a permission - Runtime: TypeScript with PostgreSQL for its own state · the builder's state is separate from the data it displays ### Data model (create this before Phase 1) - datasources(id, kind, connection_ref, scope_note, readonly, created_at) - queries(id, datasource_id, name, template, parameters_json, mutating, row_limit) - tools(id, name, layout_json, published_version_id) - bindings(widget_id, query_id, parameter_map_json) · how widget state supplies query parameters - permissions(tool_id, role, can_view, can_run_mutating, allowed_query_ids_json) - action_log(id, tool_id, query_id, actor_id, parameters_json, rows_affected, at) · every mutating run, kept - A query records whether it mutates, because that single fact drives permissions, confirmation and logging. ### Phase 1 · Queries that cannot be assembled from input Build: Store queries as templates with typed named parameters, bound by the driver rather than interpolated. Reject a template that concatenates a parameter into its text. Enforce a row limit on every query. Trap: A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. Done when: A parameter cannot alter query structure, verified with a crafted input attempting it. A template concatenating a parameter into its text is rejected at save. Sort and filter columns come from a fixed list rather than free text. Every query carries a row limit that cannot be removed at runtime. Do not build yet: No credentials scoping, widgets or permissions. ### Phase 2 · Credentials that can only do what is needed Build: Per-datasource credentials with a documented scope. Verify at connection time that a read-only datasource genuinely cannot write. Refuse to attach a mutating query to a read-only datasource. Trap: Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. Done when: A read-only datasource's credentials are verified unable to write, tested by attempting one. A mutating query cannot be attached to a read-only datasource. Each datasource records its scope as a note reviewed by a person. Credentials are never returned by any API response, asserted in the suite. Do not build yet: No widgets, permissions or logging yet. ### Phase 3 · An interface composed from results Build: A fixed widget library bound to queries, with widget state supplying parameters. Handle a query returning no rows, an error, and more rows than the widget can display. Make the data flow between widgets explicit rather than implicit. Trap: Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. Done when: Widgets bind to queries through declared bindings and the data flow is inspectable. A query returning no rows, an error or too many rows each render a stated state rather than an empty widget. A circular binding is detected at save. Changing one widget's state re-runs only the queries that depend on it. Do not build yet: No permissions, confirmation or logging yet. ### Phase 4 · Who can press the dangerous button Build: Per-tool and per-query permissions evaluated server side. A hidden button must also be an unrunnable query. Distinguish viewing a tool from running its mutating queries. Trap: Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. Done when: A user without permission cannot run a query even by calling the endpoint directly, tested with a crafted request. Viewing and mutating permissions are separate. Permission changes take effect immediately for existing sessions. A tool with no permissions configured is inaccessible rather than open. Do not build yet: No confirmation, logging or versioning yet. ### Phase 5 · Confirming and recording what changed Build: Preview the affected row count before running a mutating query and require confirmation. Log every mutating run with the actor, parameters and rows affected. Alert on a run affecting far more rows than usual. Trap: A mutating query with a mistaken parameter can update every row in a table, and the person pressing the button has no way to know until afterwards. Count the affected rows first, show that number in the confirmation, and refuse without confirmation for anything above a threshold. Done when: A mutating query previews its affected row count before running and requires confirmation. A run affecting more rows than a stated threshold requires an additional acknowledgement. Every mutating run is logged with actor, parameters and rows affected. An unusually large run alerts after the fact as well as warning before it. Do not build yet: Nothing after this. ### Out of scope (and why) - Breadth of datasource connectors, which is most of the hosted product's engineering. - Custom widget development by users, which reintroduces arbitrary code into an interface over production data. - Being a general application platform, which is where the security model stops being tractable. - Schema migration or administration, which is not something a tool builder should be able to do. ### README must contain - That parameters bind as values and structural variation comes from a fixed list, never from free text - That datasource credentials are scoped at the database and a read-only source is verified unable to write - That permissions are enforced where the query runs, since a hidden button is not a permission - That mutating queries preview their affected row count and require confirmation - That every mutating run is logged with actor, parameters and rows affected ===== AGENTS.md ===== # Agent instructions · Appsmith Cloud indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Parameterised queries only, stored as templates with typed parameters, Per-datasource, scoped to the minimum the tools need, A fixed library bound to query results, Per-tool and per-action, enforced server side, TypeScript with PostgreSQL for its own state. 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 - A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. - Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. - Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. - Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. ===== BUILD_PLAN.md ===== # Build plan · Appsmith Cloud An internal tool builder where the interesting work is composing an interface from queries and widgets, and the discipline is that everything touching a production database is parameterised, scoped, confirmed and logged. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Queries that cannot be assembled from input A parameter can never become part of the query's structure. ### Steps 1. Bind parameters as values only Structure from a fixed list, never from input. Files: `src/queries.ts`, `tests/injection.test.ts` 2. Reject concatenating templates at save Before they exist, not when they are run. Files: `src/validate.ts` 3. Enforce a row limit always An unbounded query against production is an outage. Files: `src/queries.ts` ### Done when - [ ] A crafted parameter cannot alter query structure - [ ] Concatenating templates are rejected at save - [ ] Sort and filter columns come from a fixed list - [ ] Row limits cannot be removed at runtime ### Watch out - A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. ## Phase 2 · Credentials that can only do what is needed A read-only tool uses credentials that cannot write. ### Steps 1. Verify read-only at the database Attempt a write and expect the database to refuse it. Files: `src/datasources.ts`, `tests/readonly.test.ts` 2. Refuse mutating queries on read-only sources At save, so the mistake cannot be made. Files: `src/queries.ts` 3. Never return credentials Asserted in the suite across every response shape. Files: `tests/no-credential-leak.test.ts` ### Done when - [ ] Read-only credentials are verified unable to write - [ ] Mutating queries cannot attach to read-only sources - [ ] Each datasource records a reviewed scope note - [ ] Credentials never appear in responses ### Watch out - Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. ## Phase 3 · An interface composed from results Widgets bind to query results and update predictably. ### Steps 1. Declare bindings explicitly Implicit reactivity produces tools nobody can debug. Files: `src/bindings.ts` 2. Handle empty, error and oversized results Three states that all look like a blank widget otherwise. Files: `src/widgets.ts` 3. Detect circular bindings at save A cycle at runtime is a hang rather than an error. Files: `src/bindings.ts`, `src/bindings.test.ts` ### Done when - [ ] Bindings are declared and inspectable - [ ] Empty, error and oversized results render stated states - [ ] Circular bindings are detected at save - [ ] Only dependent queries re-run on a state change ### Watch out - Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. ## Phase 4 · Who can press the dangerous button Permissions are enforced where the query runs, not where the button is drawn. ### Steps 1. Enforce at execution A hidden button is a convenience, not a permission. Files: `src/permissions.ts`, `tests/permission-bypass.test.ts` 2. Separate viewing from mutating Most people should see the tool and press nothing dangerous. Files: `src/permissions.ts` 3. Default to inaccessible An unconfigured tool should be closed, not open. Files: `src/tools.ts` ### Done when - [ ] A direct request without permission is refused - [ ] Viewing and mutating permissions are separate - [ ] Permission changes apply immediately - [ ] Unconfigured tools are inaccessible ### Watch out - Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. ## Not in this build - Breadth of datasource connectors. - Custom widget development by users. - A general application platform. - Schema migration or administration. ## After v1, if you want it - Approval workflows for high-impact actions, which is what teams ask for after the first mistake - Query result caching with explicit staleness, since internal tools reload constantly - An audit view per record showing which tools changed it, which is the question after an incident ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The builder's own state. Separate from any datasource it connects to. DATABASE_URL=postgres://localhost:5432/builder # Required · secret. Scoped per datasource at the database. Never returned by any API response. DATASOURCE_CREDENTIALS=a secrets reference # Required. Applied to every query and not removable at runtime. DEFAULT_ROW_LIMIT=1000 # Required. Above this a mutating run needs an extra acknowledgement. MUTATION_CONFIRM_THRESHOLD=50 # Required. Present as a constant. It is the vulnerability this category is prone to. ALLOW_STRING_INTERPOLATION=never # Required. Who changed what in production, kept long enough to answer the question later. ACTION_LOG_RETENTION_DAYS=730
You are building a lean indie version of Appsmith 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 ===== # Appsmith Cloud · indie build An internal tool builder where the interesting work is composing an interface from queries and widgets, and the discipline is that everything touching a production database is parameterised, scoped, confirmed and logged. 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 | | --- | --- | --- | | Data access | Parameterised queries only, stored as templates with typed parameters | string interpolation is the vulnerability this category is prone to | | Credentials | Per-datasource, scoped to the minimum the tools need | a builder with administrative access is one bug from an incident | | Widgets | A fixed library bound to query results | an unbounded widget set becomes a framework nobody can secure | | Permissions | Per-tool and per-action, enforced server side | a hidden button is not a permission | | Runtime | TypeScript with PostgreSQL for its own state | the builder's state is separate from the data it displays | ## 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 builder and the query execution layer. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: The builder's own state, and a realistic target database for the tools it builds. - Get it: Docker or your package manager, with a separate database for the builder and the target. - Verify: Two databases exist and the builder cannot reach the target with its own credentials - [ ] **Database credentials at two scope levels** · free - Why: The read-only guarantee is a database property rather than an application promise, and it has to be tested against real credentials that genuinely cannot write. - Get it: Create a read-only role and a narrowly scoped write role on your target database. - Verify: The read-only role's write attempt is refused by the database - [ ] **A realistic target database** · free - Why: Row limits, oversized results and the consequences of a mistaken parameter only become real against a table with enough rows to matter. - Get it: A copy of a real schema with at least a hundred thousand rows in the main table. - Verify: A query without a limit would return far more than a widget can display - [ ] **Your rules for mutating tools** · free - Why: The confirmation threshold and who may run mutating queries are decisions that belong before the first tool rather than after the first accident. - Get it: Decide the row count above which extra acknowledgement is required, and who may create a mutating query at all. - Verify: A written policy states both and the threshold is configured ## 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: - Breadth of datasource connectors. - Custom widget development by users. - A general application platform. - Schema migration or administration. - connectors, enterprise permissions, workflows, support, and managed hosting - hundreds of connectors - pixel-perfect app design - mobile app-store builds - enterprise permissions and scale If one of those is essential to you, that is the reason to keep paying for Appsmith Cloud, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Appsmith 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 internal tools over a real database, where a wrong click cannot destroy a table to replace Appsmith 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. ### Rule zero This connects to production data and gives people a button that changes it. Two constraints follow and both are structural. Queries are parameterised and never assembled from user input as text. A tool builder that interpolates values into a query string hands anyone who can use the tool the ability to run whatever they like against the database, and internal tools are exactly where that is least noticed because everyone using them is trusted. Second, the database connection is scoped to what the tool needs. A read-only dashboard uses read-only credentials. A tool that updates one table uses credentials that can only update that table. The alternative is a builder holding administrative access to production, which is one bug away from a very bad afternoon. The third thing, less absolute but nearly as important: destructive actions are confirmed with what they will affect, and they are logged with who did it. Internal tools accumulate buttons nobody remembers adding. ### Stack (fixed, do not substitute) - Data access: Parameterised queries only, stored as templates with typed parameters · string interpolation is the vulnerability this category is prone to - Credentials: Per-datasource, scoped to the minimum the tools need · a builder with administrative access is one bug from an incident - Widgets: A fixed library bound to query results · an unbounded widget set becomes a framework nobody can secure - Permissions: Per-tool and per-action, enforced server side · a hidden button is not a permission - Runtime: TypeScript with PostgreSQL for its own state · the builder's state is separate from the data it displays ### Data model (create this before Phase 1) - datasources(id, kind, connection_ref, scope_note, readonly, created_at) - queries(id, datasource_id, name, template, parameters_json, mutating, row_limit) - tools(id, name, layout_json, published_version_id) - bindings(widget_id, query_id, parameter_map_json) · how widget state supplies query parameters - permissions(tool_id, role, can_view, can_run_mutating, allowed_query_ids_json) - action_log(id, tool_id, query_id, actor_id, parameters_json, rows_affected, at) · every mutating run, kept - A query records whether it mutates, because that single fact drives permissions, confirmation and logging. ### Phase 1 · Queries that cannot be assembled from input Build: Store queries as templates with typed named parameters, bound by the driver rather than interpolated. Reject a template that concatenates a parameter into its text. Enforce a row limit on every query. Trap: A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. Done when: A parameter cannot alter query structure, verified with a crafted input attempting it. A template concatenating a parameter into its text is rejected at save. Sort and filter columns come from a fixed list rather than free text. Every query carries a row limit that cannot be removed at runtime. Do not build yet: No credentials scoping, widgets or permissions. ### Phase 2 · Credentials that can only do what is needed Build: Per-datasource credentials with a documented scope. Verify at connection time that a read-only datasource genuinely cannot write. Refuse to attach a mutating query to a read-only datasource. Trap: Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. Done when: A read-only datasource's credentials are verified unable to write, tested by attempting one. A mutating query cannot be attached to a read-only datasource. Each datasource records its scope as a note reviewed by a person. Credentials are never returned by any API response, asserted in the suite. Do not build yet: No widgets, permissions or logging yet. ### Phase 3 · An interface composed from results Build: A fixed widget library bound to queries, with widget state supplying parameters. Handle a query returning no rows, an error, and more rows than the widget can display. Make the data flow between widgets explicit rather than implicit. Trap: Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. Done when: Widgets bind to queries through declared bindings and the data flow is inspectable. A query returning no rows, an error or too many rows each render a stated state rather than an empty widget. A circular binding is detected at save. Changing one widget's state re-runs only the queries that depend on it. Do not build yet: No permissions, confirmation or logging yet. ### Phase 4 · Who can press the dangerous button Build: Per-tool and per-query permissions evaluated server side. A hidden button must also be an unrunnable query. Distinguish viewing a tool from running its mutating queries. Trap: Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. Done when: A user without permission cannot run a query even by calling the endpoint directly, tested with a crafted request. Viewing and mutating permissions are separate. Permission changes take effect immediately for existing sessions. A tool with no permissions configured is inaccessible rather than open. Do not build yet: No confirmation, logging or versioning yet. ### Phase 5 · Confirming and recording what changed Build: Preview the affected row count before running a mutating query and require confirmation. Log every mutating run with the actor, parameters and rows affected. Alert on a run affecting far more rows than usual. Trap: A mutating query with a mistaken parameter can update every row in a table, and the person pressing the button has no way to know until afterwards. Count the affected rows first, show that number in the confirmation, and refuse without confirmation for anything above a threshold. Done when: A mutating query previews its affected row count before running and requires confirmation. A run affecting more rows than a stated threshold requires an additional acknowledgement. Every mutating run is logged with actor, parameters and rows affected. An unusually large run alerts after the fact as well as warning before it. Do not build yet: Nothing after this. ### Out of scope (and why) - Breadth of datasource connectors, which is most of the hosted product's engineering. - Custom widget development by users, which reintroduces arbitrary code into an interface over production data. - Being a general application platform, which is where the security model stops being tractable. - Schema migration or administration, which is not something a tool builder should be able to do. ### README must contain - That parameters bind as values and structural variation comes from a fixed list, never from free text - That datasource credentials are scoped at the database and a read-only source is verified unable to write - That permissions are enforced where the query runs, since a hidden button is not a permission - That mutating queries preview their affected row count and require confirmation - That every mutating run is logged with actor, parameters and rows affected ===== AGENTS.md ===== # Agent instructions · Appsmith Cloud indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Parameterised queries only, stored as templates with typed parameters, Per-datasource, scoped to the minimum the tools need, A fixed library bound to query results, Per-tool and per-action, enforced server side, TypeScript with PostgreSQL for its own state. 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 - A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. - Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. - Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. - Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. ===== BUILD_PLAN.md ===== # Build plan · Appsmith Cloud An internal tool builder where the interesting work is composing an interface from queries and widgets, and the discipline is that everything touching a production database is parameterised, scoped, confirmed and logged. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Queries that cannot be assembled from input A parameter can never become part of the query's structure. ### Steps 1. Bind parameters as values only Structure from a fixed list, never from input. Files: `src/queries.ts`, `tests/injection.test.ts` 2. Reject concatenating templates at save Before they exist, not when they are run. Files: `src/validate.ts` 3. Enforce a row limit always An unbounded query against production is an outage. Files: `src/queries.ts` ### Done when - [ ] A crafted parameter cannot alter query structure - [ ] Concatenating templates are rejected at save - [ ] Sort and filter columns come from a fixed list - [ ] Row limits cannot be removed at runtime ### Watch out - A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. ## Phase 2 · Credentials that can only do what is needed A read-only tool uses credentials that cannot write. ### Steps 1. Verify read-only at the database Attempt a write and expect the database to refuse it. Files: `src/datasources.ts`, `tests/readonly.test.ts` 2. Refuse mutating queries on read-only sources At save, so the mistake cannot be made. Files: `src/queries.ts` 3. Never return credentials Asserted in the suite across every response shape. Files: `tests/no-credential-leak.test.ts` ### Done when - [ ] Read-only credentials are verified unable to write - [ ] Mutating queries cannot attach to read-only sources - [ ] Each datasource records a reviewed scope note - [ ] Credentials never appear in responses ### Watch out - Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. ## Phase 3 · An interface composed from results Widgets bind to query results and update predictably. ### Steps 1. Declare bindings explicitly Implicit reactivity produces tools nobody can debug. Files: `src/bindings.ts` 2. Handle empty, error and oversized results Three states that all look like a blank widget otherwise. Files: `src/widgets.ts` 3. Detect circular bindings at save A cycle at runtime is a hang rather than an error. Files: `src/bindings.ts`, `src/bindings.test.ts` ### Done when - [ ] Bindings are declared and inspectable - [ ] Empty, error and oversized results render stated states - [ ] Circular bindings are detected at save - [ ] Only dependent queries re-run on a state change ### Watch out - Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. ## Phase 4 · Who can press the dangerous button Permissions are enforced where the query runs, not where the button is drawn. ### Steps 1. Enforce at execution A hidden button is a convenience, not a permission. Files: `src/permissions.ts`, `tests/permission-bypass.test.ts` 2. Separate viewing from mutating Most people should see the tool and press nothing dangerous. Files: `src/permissions.ts` 3. Default to inaccessible An unconfigured tool should be closed, not open. Files: `src/tools.ts` ### Done when - [ ] A direct request without permission is refused - [ ] Viewing and mutating permissions are separate - [ ] Permission changes apply immediately - [ ] Unconfigured tools are inaccessible ### Watch out - Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. ## Not in this build - Breadth of datasource connectors. - Custom widget development by users. - A general application platform. - Schema migration or administration. ## After v1, if you want it - Approval workflows for high-impact actions, which is what teams ask for after the first mistake - Query result caching with explicit staleness, since internal tools reload constantly - An audit view per record showing which tools changed it, which is the question after an incident ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The builder's own state. Separate from any datasource it connects to. DATABASE_URL=postgres://localhost:5432/builder # Required · secret. Scoped per datasource at the database. Never returned by any API response. DATASOURCE_CREDENTIALS=a secrets reference # Required. Applied to every query and not removable at runtime. DEFAULT_ROW_LIMIT=1000 # Required. Above this a mutating run needs an extra acknowledgement. MUTATION_CONFIRM_THRESHOLD=50 # Required. Present as a constant. It is the vulnerability this category is prone to. ALLOW_STRING_INTERPOLATION=never # Required. Who changed what in production, kept long enough to answer the question later. ACTION_LOG_RETENTION_DAYS=730
You are building a production product version of Appsmith 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 ===== # Appsmith Cloud · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Appsmith Cloud, compose an internal dashboard from a database and a fixed widget library. The hard boundary is connectors, enterprise permissions, workflows, support, and managed hosting, plus connectors, runtime reliability, and governance. ## Product outcome Internal tools built over production data where a parameter cannot become a query, a read-only dashboard genuinely cannot write, and every destructive action states its effect before running and is recorded after. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - self-hosted deploy target - API credentials for one selected integration ## Explicit non-goals for v1 - Breadth of datasource connectors. - Custom widget development by users. - A general application platform. - Schema migration or administration. - connectors, enterprise permissions, workflows, support, and managed hosting - hundreds of connectors - pixel-perfect app design - mobile app-store builds - enterprise permissions and scale ## Success criteria - A crafted parameter is confirmed unable to alter query structure - Read-only credentials are confirmed unable to write - A direct request without permission is confirmed refused - Mutating queries are confirmed to preview affected rows - Every mutating run is confirmed logged with its actor ===== BRIEF.md ===== # Build brief · Appsmith 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 internal tools over a real database, where a wrong click cannot destroy a table to replace Appsmith 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. ### Rule zero This connects to production data and gives people a button that changes it. Two constraints follow and both are structural. Queries are parameterised and never assembled from user input as text. A tool builder that interpolates values into a query string hands anyone who can use the tool the ability to run whatever they like against the database, and internal tools are exactly where that is least noticed because everyone using them is trusted. Second, the database connection is scoped to what the tool needs. A read-only dashboard uses read-only credentials. A tool that updates one table uses credentials that can only update that table. The alternative is a builder holding administrative access to production, which is one bug away from a very bad afternoon. The third thing, less absolute but nearly as important: destructive actions are confirmed with what they will affect, and they are logged with who did it. Internal tools accumulate buttons nobody remembers adding. ### Stack (fixed, do not substitute) - Data access: Parameterised queries only, stored as templates with typed parameters · string interpolation is the vulnerability this category is prone to - Credentials: Per-datasource, scoped to the minimum the tools need · a builder with administrative access is one bug from an incident - Widgets: A fixed library bound to query results · an unbounded widget set becomes a framework nobody can secure - Permissions: Per-tool and per-action, enforced server side · a hidden button is not a permission - Runtime: TypeScript with PostgreSQL for its own state · the builder's state is separate from the data it displays ### Data model (create this before Phase 1) - datasources(id, kind, connection_ref, scope_note, readonly, created_at) - queries(id, datasource_id, name, template, parameters_json, mutating, row_limit) - tools(id, name, layout_json, published_version_id) - bindings(widget_id, query_id, parameter_map_json) · how widget state supplies query parameters - permissions(tool_id, role, can_view, can_run_mutating, allowed_query_ids_json) - action_log(id, tool_id, query_id, actor_id, parameters_json, rows_affected, at) · every mutating run, kept - A query records whether it mutates, because that single fact drives permissions, confirmation and logging. ### Phase 1 · Queries that cannot be assembled from input Build: Store queries as templates with typed named parameters, bound by the driver rather than interpolated. Reject a template that concatenates a parameter into its text. Enforce a row limit on every query. Trap: A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. Done when: A parameter cannot alter query structure, verified with a crafted input attempting it. A template concatenating a parameter into its text is rejected at save. Sort and filter columns come from a fixed list rather than free text. Every query carries a row limit that cannot be removed at runtime. Do not build yet: No credentials scoping, widgets or permissions. ### Phase 2 · Credentials that can only do what is needed Build: Per-datasource credentials with a documented scope. Verify at connection time that a read-only datasource genuinely cannot write. Refuse to attach a mutating query to a read-only datasource. Trap: Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. Done when: A read-only datasource's credentials are verified unable to write, tested by attempting one. A mutating query cannot be attached to a read-only datasource. Each datasource records its scope as a note reviewed by a person. Credentials are never returned by any API response, asserted in the suite. Do not build yet: No widgets, permissions or logging yet. ### Phase 3 · An interface composed from results Build: A fixed widget library bound to queries, with widget state supplying parameters. Handle a query returning no rows, an error, and more rows than the widget can display. Make the data flow between widgets explicit rather than implicit. Trap: Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. Done when: Widgets bind to queries through declared bindings and the data flow is inspectable. A query returning no rows, an error or too many rows each render a stated state rather than an empty widget. A circular binding is detected at save. Changing one widget's state re-runs only the queries that depend on it. Do not build yet: No permissions, confirmation or logging yet. ### Phase 4 · Who can press the dangerous button Build: Per-tool and per-query permissions evaluated server side. A hidden button must also be an unrunnable query. Distinguish viewing a tool from running its mutating queries. Trap: Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. Done when: A user without permission cannot run a query even by calling the endpoint directly, tested with a crafted request. Viewing and mutating permissions are separate. Permission changes take effect immediately for existing sessions. A tool with no permissions configured is inaccessible rather than open. Do not build yet: No confirmation, logging or versioning yet. ### Phase 5 · Confirming and recording what changed Build: Preview the affected row count before running a mutating query and require confirmation. Log every mutating run with the actor, parameters and rows affected. Alert on a run affecting far more rows than usual. Trap: A mutating query with a mistaken parameter can update every row in a table, and the person pressing the button has no way to know until afterwards. Count the affected rows first, show that number in the confirmation, and refuse without confirmation for anything above a threshold. Done when: A mutating query previews its affected row count before running and requires confirmation. A run affecting more rows than a stated threshold requires an additional acknowledgement. Every mutating run is logged with actor, parameters and rows affected. An unusually large run alerts after the fact as well as warning before it. Do not build yet: Nothing after this. ### Out of scope (and why) - Breadth of datasource connectors, which is most of the hosted product's engineering. - Custom widget development by users, which reintroduces arbitrary code into an interface over production data. - Being a general application platform, which is where the security model stops being tractable. - Schema migration or administration, which is not something a tool builder should be able to do. ### README must contain - That parameters bind as values and structural variation comes from a fixed list, never from free text - That datasource credentials are scoped at the database and a read-only source is verified unable to write - That permissions are enforced where the query runs, since a hidden button is not a permission - That mutating queries preview their affected row count and require confirmation - That every mutating run is logged with actor, parameters and rows affected ===== ARCHITECTURE.md ===== # Architecture · Appsmith Cloud ## Stack | Part | Choice | Why | | --- | --- | --- | | Data access | Parameterised queries only, stored as templates with typed parameters | string interpolation is the vulnerability this category is prone to | | Credentials | Per-datasource, scoped to the minimum the tools need | a builder with administrative access is one bug from an incident | | Widgets | A fixed library bound to query results | an unbounded widget set becomes a framework nobody can secure | | Permissions | Per-tool and per-action, enforced server side | a hidden button is not a permission | | Runtime | TypeScript with PostgreSQL for its own state | the builder's state is separate from the data it displays | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Queries | Parameterised templates with fixed structural choices. | Not swappable for interpolation, which is the vulnerability of the category. | | Datasources | Scoped credentials verified at the database. | Any database, provided scope is enforced there rather than promised in the application. | | Bindings | Explicit data flow between widgets and queries. | Any model, provided the flow is inspectable and cycles are detected. | | Permissions | Execution-time enforcement. | Not swappable for hidden interface elements, which are not a control. | | Action log | Confirmation previews and mutation records. | Any store, provided every mutating run is recorded with its actor. | ## 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 · The builder's own state. Separate from any datasource it connects to. - `DATASOURCE_CREDENTIALS` · required, secret · Scoped per datasource at the database. Never returned by any API response. - `DEFAULT_ROW_LIMIT` · required · Applied to every query and not removable at runtime. - `MUTATION_CONFIRM_THRESHOLD` · required · Above this a mutating run needs an extra acknowledgement. - `ALLOW_STRING_INTERPOLATION` · required · Present as a constant. It is the vulnerability this category is prone to. - `ACTION_LOG_RETENTION_DAYS` · required · Who changed what in production, kept long enough to answer the question later. ## 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 · Appsmith Cloud product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Parameterised queries only, stored as templates with typed parameters, Per-datasource, scoped to the minimum the tools need, A fixed library bound to query results, Per-tool and per-action, enforced server side, TypeScript with PostgreSQL for its own state. - 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 - A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. - Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. - Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. - Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. - A mutating query with a mistaken parameter can update every row in a table, and the person pressing the button has no way to know until afterwards. Count the affected rows first, show that number in the confirmation, and refuse without confirmation for anything above a threshold. ===== MILESTONES.md ===== # Delivery milestones · Appsmith Cloud Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Queries that cannot be assembled from input A parameter can never become part of the query's structure. ### Steps 1. Bind parameters as values only Structure from a fixed list, never from input. Files: `src/queries.ts`, `tests/injection.test.ts` 2. Reject concatenating templates at save Before they exist, not when they are run. Files: `src/validate.ts` 3. Enforce a row limit always An unbounded query against production is an outage. Files: `src/queries.ts` ### Done when - [ ] A crafted parameter cannot alter query structure - [ ] Concatenating templates are rejected at save - [ ] Sort and filter columns come from a fixed list - [ ] Row limits cannot be removed at runtime ### Watch out - A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. ## M2 · Credentials that can only do what is needed A read-only tool uses credentials that cannot write. ### Steps 1. Verify read-only at the database Attempt a write and expect the database to refuse it. Files: `src/datasources.ts`, `tests/readonly.test.ts` 2. Refuse mutating queries on read-only sources At save, so the mistake cannot be made. Files: `src/queries.ts` 3. Never return credentials Asserted in the suite across every response shape. Files: `tests/no-credential-leak.test.ts` ### Done when - [ ] Read-only credentials are verified unable to write - [ ] Mutating queries cannot attach to read-only sources - [ ] Each datasource records a reviewed scope note - [ ] Credentials never appear in responses ### Watch out - Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. ## M3 · An interface composed from results Widgets bind to query results and update predictably. ### Steps 1. Declare bindings explicitly Implicit reactivity produces tools nobody can debug. Files: `src/bindings.ts` 2. Handle empty, error and oversized results Three states that all look like a blank widget otherwise. Files: `src/widgets.ts` 3. Detect circular bindings at save A cycle at runtime is a hang rather than an error. Files: `src/bindings.ts`, `src/bindings.test.ts` ### Done when - [ ] Bindings are declared and inspectable - [ ] Empty, error and oversized results render stated states - [ ] Circular bindings are detected at save - [ ] Only dependent queries re-run on a state change ### Watch out - Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. ## M4 · Who can press the dangerous button Permissions are enforced where the query runs, not where the button is drawn. ### Steps 1. Enforce at execution A hidden button is a convenience, not a permission. Files: `src/permissions.ts`, `tests/permission-bypass.test.ts` 2. Separate viewing from mutating Most people should see the tool and press nothing dangerous. Files: `src/permissions.ts` 3. Default to inaccessible An unconfigured tool should be closed, not open. Files: `src/tools.ts` ### Done when - [ ] A direct request without permission is refused - [ ] Viewing and mutating permissions are separate - [ ] Permission changes apply immediately - [ ] Unconfigured tools are inaccessible ### Watch out - Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. ## M5 · Confirming and recording what changed (production only) Every mutating action states its effect first and is logged after. ### Steps 1. Count affected rows before running The person pressing the button cannot otherwise know. Files: `src/preview.ts` 2. Log every mutating run Internal tools accumulate buttons nobody remembers adding. Files: `src/action-log.ts` 3. Alert on unusually large effects Before by threshold, and after by comparison to the usual. Files: `src/alerts.ts` ### Done when - [ ] Mutating queries preview affected rows - [ ] Large effects require an extra acknowledgement - [ ] Every mutating run is logged with actor and parameters - [ ] Unusually large runs alert ### Watch out - A mutating query with a mistaken parameter can update every row in a table, and the person pressing the button has no way to know until afterwards. Count the affected rows first, show that number in the confirmation, and refuse without confirmation for anything above a threshold. ===== OPERATIONS.md ===== # Operations · Appsmith Cloud ## Backup The builder's own state holds tool definitions and the action log, both of which matter. The target data belongs to whoever owns that database and is their responsibility, which is worth being explicit about. ## Restore Restore and confirm datasource credentials were not restored from an old backup with wider scope than the current ones, since a widened scope after a restore is invisible until something writes. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Rows affected per mutating run, compared against that query's usual. A run affecting a thousand times more rows than normal is the shape of a mistaken parameter and is visible immediately. ## Incident checklist A tool that updated more rows than intended is the incident, and the action log with parameters and counts is what turns recovery into a targeted fix rather than a restore from backup. 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 - [ ] A crafted parameter is confirmed unable to alter query structure - [ ] Read-only credentials are confirmed unable to write - [ ] A direct request without permission is confirmed refused - [ ] Mutating queries are confirmed to preview affected rows - [ ] Every mutating run is confirmed logged with its actor ## Launch constraint Do not market omitted Appsmith 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. The builder's own state. Separate from any datasource it connects to. DATABASE_URL=postgres://localhost:5432/builder # Required · secret. Scoped per datasource at the database. Never returned by any API response. DATASOURCE_CREDENTIALS=a secrets reference # Required. Applied to every query and not removable at runtime. DEFAULT_ROW_LIMIT=1000 # Required. Above this a mutating run needs an extra acknowledgement. MUTATION_CONFIRM_THRESHOLD=50 # Required. Present as a constant. It is the vulnerability this category is prone to. ALLOW_STRING_INTERPOLATION=never # Required. Who changed what in production, kept long enough to answer the question later. ACTION_LOG_RETENTION_DAYS=730
# Appsmith Cloud · indie build An internal tool builder where the interesting work is composing an interface from queries and widgets, and the discipline is that everything touching a production database is parameterised, scoped, confirmed and logged. 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 | | --- | --- | --- | | Data access | Parameterised queries only, stored as templates with typed parameters | string interpolation is the vulnerability this category is prone to | | Credentials | Per-datasource, scoped to the minimum the tools need | a builder with administrative access is one bug from an incident | | Widgets | A fixed library bound to query results | an unbounded widget set becomes a framework nobody can secure | | Permissions | Per-tool and per-action, enforced server side | a hidden button is not a permission | | Runtime | TypeScript with PostgreSQL for its own state | the builder's state is separate from the data it displays | ## 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 builder and the query execution layer. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: The builder's own state, and a realistic target database for the tools it builds. - Get it: Docker or your package manager, with a separate database for the builder and the target. - Verify: Two databases exist and the builder cannot reach the target with its own credentials - [ ] **Database credentials at two scope levels** · free - Why: The read-only guarantee is a database property rather than an application promise, and it has to be tested against real credentials that genuinely cannot write. - Get it: Create a read-only role and a narrowly scoped write role on your target database. - Verify: The read-only role's write attempt is refused by the database - [ ] **A realistic target database** · free - Why: Row limits, oversized results and the consequences of a mistaken parameter only become real against a table with enough rows to matter. - Get it: A copy of a real schema with at least a hundred thousand rows in the main table. - Verify: A query without a limit would return far more than a widget can display - [ ] **Your rules for mutating tools** · free - Why: The confirmation threshold and who may run mutating queries are decisions that belong before the first tool rather than after the first accident. - Get it: Decide the row count above which extra acknowledgement is required, and who may create a mutating query at all. - Verify: A written policy states both and the threshold is configured ## 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: - Breadth of datasource connectors. - Custom widget development by users. - A general application platform. - Schema migration or administration. - connectors, enterprise permissions, workflows, support, and managed hosting - hundreds of connectors - pixel-perfect app design - mobile app-store builds - enterprise permissions and scale If one of those is essential to you, that is the reason to keep paying for Appsmith Cloud, and the README should say so rather than pretend.
# Build brief · Appsmith 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 internal tools over a real database, where a wrong click cannot destroy a table to replace Appsmith 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. ### Rule zero This connects to production data and gives people a button that changes it. Two constraints follow and both are structural. Queries are parameterised and never assembled from user input as text. A tool builder that interpolates values into a query string hands anyone who can use the tool the ability to run whatever they like against the database, and internal tools are exactly where that is least noticed because everyone using them is trusted. Second, the database connection is scoped to what the tool needs. A read-only dashboard uses read-only credentials. A tool that updates one table uses credentials that can only update that table. The alternative is a builder holding administrative access to production, which is one bug away from a very bad afternoon. The third thing, less absolute but nearly as important: destructive actions are confirmed with what they will affect, and they are logged with who did it. Internal tools accumulate buttons nobody remembers adding. ### Stack (fixed, do not substitute) - Data access: Parameterised queries only, stored as templates with typed parameters · string interpolation is the vulnerability this category is prone to - Credentials: Per-datasource, scoped to the minimum the tools need · a builder with administrative access is one bug from an incident - Widgets: A fixed library bound to query results · an unbounded widget set becomes a framework nobody can secure - Permissions: Per-tool and per-action, enforced server side · a hidden button is not a permission - Runtime: TypeScript with PostgreSQL for its own state · the builder's state is separate from the data it displays ### Data model (create this before Phase 1) - datasources(id, kind, connection_ref, scope_note, readonly, created_at) - queries(id, datasource_id, name, template, parameters_json, mutating, row_limit) - tools(id, name, layout_json, published_version_id) - bindings(widget_id, query_id, parameter_map_json) · how widget state supplies query parameters - permissions(tool_id, role, can_view, can_run_mutating, allowed_query_ids_json) - action_log(id, tool_id, query_id, actor_id, parameters_json, rows_affected, at) · every mutating run, kept - A query records whether it mutates, because that single fact drives permissions, confirmation and logging. ### Phase 1 · Queries that cannot be assembled from input Build: Store queries as templates with typed named parameters, bound by the driver rather than interpolated. Reject a template that concatenates a parameter into its text. Enforce a row limit on every query. Trap: A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. Done when: A parameter cannot alter query structure, verified with a crafted input attempting it. A template concatenating a parameter into its text is rejected at save. Sort and filter columns come from a fixed list rather than free text. Every query carries a row limit that cannot be removed at runtime. Do not build yet: No credentials scoping, widgets or permissions. ### Phase 2 · Credentials that can only do what is needed Build: Per-datasource credentials with a documented scope. Verify at connection time that a read-only datasource genuinely cannot write. Refuse to attach a mutating query to a read-only datasource. Trap: Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. Done when: A read-only datasource's credentials are verified unable to write, tested by attempting one. A mutating query cannot be attached to a read-only datasource. Each datasource records its scope as a note reviewed by a person. Credentials are never returned by any API response, asserted in the suite. Do not build yet: No widgets, permissions or logging yet. ### Phase 3 · An interface composed from results Build: A fixed widget library bound to queries, with widget state supplying parameters. Handle a query returning no rows, an error, and more rows than the widget can display. Make the data flow between widgets explicit rather than implicit. Trap: Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. Done when: Widgets bind to queries through declared bindings and the data flow is inspectable. A query returning no rows, an error or too many rows each render a stated state rather than an empty widget. A circular binding is detected at save. Changing one widget's state re-runs only the queries that depend on it. Do not build yet: No permissions, confirmation or logging yet. ### Phase 4 · Who can press the dangerous button Build: Per-tool and per-query permissions evaluated server side. A hidden button must also be an unrunnable query. Distinguish viewing a tool from running its mutating queries. Trap: Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. Done when: A user without permission cannot run a query even by calling the endpoint directly, tested with a crafted request. Viewing and mutating permissions are separate. Permission changes take effect immediately for existing sessions. A tool with no permissions configured is inaccessible rather than open. Do not build yet: No confirmation, logging or versioning yet. ### Phase 5 · Confirming and recording what changed Build: Preview the affected row count before running a mutating query and require confirmation. Log every mutating run with the actor, parameters and rows affected. Alert on a run affecting far more rows than usual. Trap: A mutating query with a mistaken parameter can update every row in a table, and the person pressing the button has no way to know until afterwards. Count the affected rows first, show that number in the confirmation, and refuse without confirmation for anything above a threshold. Done when: A mutating query previews its affected row count before running and requires confirmation. A run affecting more rows than a stated threshold requires an additional acknowledgement. Every mutating run is logged with actor, parameters and rows affected. An unusually large run alerts after the fact as well as warning before it. Do not build yet: Nothing after this. ### Out of scope (and why) - Breadth of datasource connectors, which is most of the hosted product's engineering. - Custom widget development by users, which reintroduces arbitrary code into an interface over production data. - Being a general application platform, which is where the security model stops being tractable. - Schema migration or administration, which is not something a tool builder should be able to do. ### README must contain - That parameters bind as values and structural variation comes from a fixed list, never from free text - That datasource credentials are scoped at the database and a read-only source is verified unable to write - That permissions are enforced where the query runs, since a hidden button is not a permission - That mutating queries preview their affected row count and require confirmation - That every mutating run is logged with actor, parameters and rows affected
# Agent instructions · Appsmith Cloud indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Parameterised queries only, stored as templates with typed parameters, Per-datasource, scoped to the minimum the tools need, A fixed library bound to query results, Per-tool and per-action, enforced server side, TypeScript with PostgreSQL for its own state. 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 - A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. - Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. - Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. - Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control.
# Build plan · Appsmith Cloud An internal tool builder where the interesting work is composing an interface from queries and widgets, and the discipline is that everything touching a production database is parameterised, scoped, confirmed and logged. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Queries that cannot be assembled from input A parameter can never become part of the query's structure. ### Steps 1. Bind parameters as values only Structure from a fixed list, never from input. Files: `src/queries.ts`, `tests/injection.test.ts` 2. Reject concatenating templates at save Before they exist, not when they are run. Files: `src/validate.ts` 3. Enforce a row limit always An unbounded query against production is an outage. Files: `src/queries.ts` ### Done when - [ ] A crafted parameter cannot alter query structure - [ ] Concatenating templates are rejected at save - [ ] Sort and filter columns come from a fixed list - [ ] Row limits cannot be removed at runtime ### Watch out - A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. ## Phase 2 · Credentials that can only do what is needed A read-only tool uses credentials that cannot write. ### Steps 1. Verify read-only at the database Attempt a write and expect the database to refuse it. Files: `src/datasources.ts`, `tests/readonly.test.ts` 2. Refuse mutating queries on read-only sources At save, so the mistake cannot be made. Files: `src/queries.ts` 3. Never return credentials Asserted in the suite across every response shape. Files: `tests/no-credential-leak.test.ts` ### Done when - [ ] Read-only credentials are verified unable to write - [ ] Mutating queries cannot attach to read-only sources - [ ] Each datasource records a reviewed scope note - [ ] Credentials never appear in responses ### Watch out - Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. ## Phase 3 · An interface composed from results Widgets bind to query results and update predictably. ### Steps 1. Declare bindings explicitly Implicit reactivity produces tools nobody can debug. Files: `src/bindings.ts` 2. Handle empty, error and oversized results Three states that all look like a blank widget otherwise. Files: `src/widgets.ts` 3. Detect circular bindings at save A cycle at runtime is a hang rather than an error. Files: `src/bindings.ts`, `src/bindings.test.ts` ### Done when - [ ] Bindings are declared and inspectable - [ ] Empty, error and oversized results render stated states - [ ] Circular bindings are detected at save - [ ] Only dependent queries re-run on a state change ### Watch out - Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. ## Phase 4 · Who can press the dangerous button Permissions are enforced where the query runs, not where the button is drawn. ### Steps 1. Enforce at execution A hidden button is a convenience, not a permission. Files: `src/permissions.ts`, `tests/permission-bypass.test.ts` 2. Separate viewing from mutating Most people should see the tool and press nothing dangerous. Files: `src/permissions.ts` 3. Default to inaccessible An unconfigured tool should be closed, not open. Files: `src/tools.ts` ### Done when - [ ] A direct request without permission is refused - [ ] Viewing and mutating permissions are separate - [ ] Permission changes apply immediately - [ ] Unconfigured tools are inaccessible ### Watch out - Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. ## Not in this build - Breadth of datasource connectors. - Custom widget development by users. - A general application platform. - Schema migration or administration. ## After v1, if you want it - Approval workflows for high-impact actions, which is what teams ask for after the first mistake - Query result caching with explicit staleness, since internal tools reload constantly - An audit view per record showing which tools changed it, which is the question after an incident
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The builder's own state. Separate from any datasource it connects to. DATABASE_URL=postgres://localhost:5432/builder # Required · secret. Scoped per datasource at the database. Never returned by any API response. DATASOURCE_CREDENTIALS=a secrets reference # Required. Applied to every query and not removable at runtime. DEFAULT_ROW_LIMIT=1000 # Required. Above this a mutating run needs an extra acknowledgement. MUTATION_CONFIRM_THRESHOLD=50 # Required. Present as a constant. It is the vulnerability this category is prone to. ALLOW_STRING_INTERPOLATION=never # Required. Who changed what in production, kept long enough to answer the question later. ACTION_LOG_RETENTION_DAYS=730
# Appsmith Cloud · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Appsmith Cloud, compose an internal dashboard from a database and a fixed widget library. The hard boundary is connectors, enterprise permissions, workflows, support, and managed hosting, plus connectors, runtime reliability, and governance. ## Product outcome Internal tools built over production data where a parameter cannot become a query, a read-only dashboard genuinely cannot write, and every destructive action states its effect before running and is recorded after. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - self-hosted deploy target - API credentials for one selected integration ## Explicit non-goals for v1 - Breadth of datasource connectors. - Custom widget development by users. - A general application platform. - Schema migration or administration. - connectors, enterprise permissions, workflows, support, and managed hosting - hundreds of connectors - pixel-perfect app design - mobile app-store builds - enterprise permissions and scale ## Success criteria - A crafted parameter is confirmed unable to alter query structure - Read-only credentials are confirmed unable to write - A direct request without permission is confirmed refused - Mutating queries are confirmed to preview affected rows - Every mutating run is confirmed logged with its actor
# Build brief · Appsmith 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 internal tools over a real database, where a wrong click cannot destroy a table to replace Appsmith 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. ### Rule zero This connects to production data and gives people a button that changes it. Two constraints follow and both are structural. Queries are parameterised and never assembled from user input as text. A tool builder that interpolates values into a query string hands anyone who can use the tool the ability to run whatever they like against the database, and internal tools are exactly where that is least noticed because everyone using them is trusted. Second, the database connection is scoped to what the tool needs. A read-only dashboard uses read-only credentials. A tool that updates one table uses credentials that can only update that table. The alternative is a builder holding administrative access to production, which is one bug away from a very bad afternoon. The third thing, less absolute but nearly as important: destructive actions are confirmed with what they will affect, and they are logged with who did it. Internal tools accumulate buttons nobody remembers adding. ### Stack (fixed, do not substitute) - Data access: Parameterised queries only, stored as templates with typed parameters · string interpolation is the vulnerability this category is prone to - Credentials: Per-datasource, scoped to the minimum the tools need · a builder with administrative access is one bug from an incident - Widgets: A fixed library bound to query results · an unbounded widget set becomes a framework nobody can secure - Permissions: Per-tool and per-action, enforced server side · a hidden button is not a permission - Runtime: TypeScript with PostgreSQL for its own state · the builder's state is separate from the data it displays ### Data model (create this before Phase 1) - datasources(id, kind, connection_ref, scope_note, readonly, created_at) - queries(id, datasource_id, name, template, parameters_json, mutating, row_limit) - tools(id, name, layout_json, published_version_id) - bindings(widget_id, query_id, parameter_map_json) · how widget state supplies query parameters - permissions(tool_id, role, can_view, can_run_mutating, allowed_query_ids_json) - action_log(id, tool_id, query_id, actor_id, parameters_json, rows_affected, at) · every mutating run, kept - A query records whether it mutates, because that single fact drives permissions, confirmation and logging. ### Phase 1 · Queries that cannot be assembled from input Build: Store queries as templates with typed named parameters, bound by the driver rather than interpolated. Reject a template that concatenates a parameter into its text. Enforce a row limit on every query. Trap: A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. Done when: A parameter cannot alter query structure, verified with a crafted input attempting it. A template concatenating a parameter into its text is rejected at save. Sort and filter columns come from a fixed list rather than free text. Every query carries a row limit that cannot be removed at runtime. Do not build yet: No credentials scoping, widgets or permissions. ### Phase 2 · Credentials that can only do what is needed Build: Per-datasource credentials with a documented scope. Verify at connection time that a read-only datasource genuinely cannot write. Refuse to attach a mutating query to a read-only datasource. Trap: Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. Done when: A read-only datasource's credentials are verified unable to write, tested by attempting one. A mutating query cannot be attached to a read-only datasource. Each datasource records its scope as a note reviewed by a person. Credentials are never returned by any API response, asserted in the suite. Do not build yet: No widgets, permissions or logging yet. ### Phase 3 · An interface composed from results Build: A fixed widget library bound to queries, with widget state supplying parameters. Handle a query returning no rows, an error, and more rows than the widget can display. Make the data flow between widgets explicit rather than implicit. Trap: Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. Done when: Widgets bind to queries through declared bindings and the data flow is inspectable. A query returning no rows, an error or too many rows each render a stated state rather than an empty widget. A circular binding is detected at save. Changing one widget's state re-runs only the queries that depend on it. Do not build yet: No permissions, confirmation or logging yet. ### Phase 4 · Who can press the dangerous button Build: Per-tool and per-query permissions evaluated server side. A hidden button must also be an unrunnable query. Distinguish viewing a tool from running its mutating queries. Trap: Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. Done when: A user without permission cannot run a query even by calling the endpoint directly, tested with a crafted request. Viewing and mutating permissions are separate. Permission changes take effect immediately for existing sessions. A tool with no permissions configured is inaccessible rather than open. Do not build yet: No confirmation, logging or versioning yet. ### Phase 5 · Confirming and recording what changed Build: Preview the affected row count before running a mutating query and require confirmation. Log every mutating run with the actor, parameters and rows affected. Alert on a run affecting far more rows than usual. Trap: A mutating query with a mistaken parameter can update every row in a table, and the person pressing the button has no way to know until afterwards. Count the affected rows first, show that number in the confirmation, and refuse without confirmation for anything above a threshold. Done when: A mutating query previews its affected row count before running and requires confirmation. A run affecting more rows than a stated threshold requires an additional acknowledgement. Every mutating run is logged with actor, parameters and rows affected. An unusually large run alerts after the fact as well as warning before it. Do not build yet: Nothing after this. ### Out of scope (and why) - Breadth of datasource connectors, which is most of the hosted product's engineering. - Custom widget development by users, which reintroduces arbitrary code into an interface over production data. - Being a general application platform, which is where the security model stops being tractable. - Schema migration or administration, which is not something a tool builder should be able to do. ### README must contain - That parameters bind as values and structural variation comes from a fixed list, never from free text - That datasource credentials are scoped at the database and a read-only source is verified unable to write - That permissions are enforced where the query runs, since a hidden button is not a permission - That mutating queries preview their affected row count and require confirmation - That every mutating run is logged with actor, parameters and rows affected
# Architecture · Appsmith Cloud ## Stack | Part | Choice | Why | | --- | --- | --- | | Data access | Parameterised queries only, stored as templates with typed parameters | string interpolation is the vulnerability this category is prone to | | Credentials | Per-datasource, scoped to the minimum the tools need | a builder with administrative access is one bug from an incident | | Widgets | A fixed library bound to query results | an unbounded widget set becomes a framework nobody can secure | | Permissions | Per-tool and per-action, enforced server side | a hidden button is not a permission | | Runtime | TypeScript with PostgreSQL for its own state | the builder's state is separate from the data it displays | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Queries | Parameterised templates with fixed structural choices. | Not swappable for interpolation, which is the vulnerability of the category. | | Datasources | Scoped credentials verified at the database. | Any database, provided scope is enforced there rather than promised in the application. | | Bindings | Explicit data flow between widgets and queries. | Any model, provided the flow is inspectable and cycles are detected. | | Permissions | Execution-time enforcement. | Not swappable for hidden interface elements, which are not a control. | | Action log | Confirmation previews and mutation records. | Any store, provided every mutating run is recorded with its actor. | ## 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 · The builder's own state. Separate from any datasource it connects to. - `DATASOURCE_CREDENTIALS` · required, secret · Scoped per datasource at the database. Never returned by any API response. - `DEFAULT_ROW_LIMIT` · required · Applied to every query and not removable at runtime. - `MUTATION_CONFIRM_THRESHOLD` · required · Above this a mutating run needs an extra acknowledgement. - `ALLOW_STRING_INTERPOLATION` · required · Present as a constant. It is the vulnerability this category is prone to. - `ACTION_LOG_RETENTION_DAYS` · required · Who changed what in production, kept long enough to answer the question later. ## 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 · Appsmith Cloud product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Parameterised queries only, stored as templates with typed parameters, Per-datasource, scoped to the minimum the tools need, A fixed library bound to query results, Per-tool and per-action, enforced server side, TypeScript with PostgreSQL for its own state. - 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 - A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. - Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. - Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. - Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. - A mutating query with a mistaken parameter can update every row in a table, and the person pressing the button has no way to know until afterwards. Count the affected rows first, show that number in the confirmation, and refuse without confirmation for anything above a threshold.
# Delivery milestones · Appsmith Cloud Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Queries that cannot be assembled from input A parameter can never become part of the query's structure. ### Steps 1. Bind parameters as values only Structure from a fixed list, never from input. Files: `src/queries.ts`, `tests/injection.test.ts` 2. Reject concatenating templates at save Before they exist, not when they are run. Files: `src/validate.ts` 3. Enforce a row limit always An unbounded query against production is an outage. Files: `src/queries.ts` ### Done when - [ ] A crafted parameter cannot alter query structure - [ ] Concatenating templates are rejected at save - [ ] Sort and filter columns come from a fixed list - [ ] Row limits cannot be removed at runtime ### Watch out - A tool builder that lets a query be assembled from a text field, even for something innocent like a sort column, gives every user of that tool the ability to run arbitrary statements, and internal tools are where this goes unnoticed longest because everyone is trusted. Parameters bind as values only, and structural variation is a fixed list of choices rather than free text. ## M2 · Credentials that can only do what is needed A read-only tool uses credentials that cannot write. ### Steps 1. Verify read-only at the database Attempt a write and expect the database to refuse it. Files: `src/datasources.ts`, `tests/readonly.test.ts` 2. Refuse mutating queries on read-only sources At save, so the mistake cannot be made. Files: `src/queries.ts` 3. Never return credentials Asserted in the suite across every response shape. Files: `tests/no-credential-leak.test.ts` ### Done when - [ ] Read-only credentials are verified unable to write - [ ] Mutating queries cannot attach to read-only sources - [ ] Each datasource records a reviewed scope note - [ ] Credentials never appear in responses ### Watch out - Using one administrative connection for every datasource because it is simpler means a read-only dashboard is one query away from writing to production, and the person who adds that query will not think of it as a change in risk. Scope the credentials at the database rather than promising it in the application. ## M3 · An interface composed from results Widgets bind to query results and update predictably. ### Steps 1. Declare bindings explicitly Implicit reactivity produces tools nobody can debug. Files: `src/bindings.ts` 2. Handle empty, error and oversized results Three states that all look like a blank widget otherwise. Files: `src/widgets.ts` 3. Detect circular bindings at save A cycle at runtime is a hang rather than an error. Files: `src/bindings.ts`, `src/bindings.test.ts` ### Done when - [ ] Bindings are declared and inspectable - [ ] Empty, error and oversized results render stated states - [ ] Circular bindings are detected at save - [ ] Only dependent queries re-run on a state change ### Watch out - Implicit reactivity where any widget can read any other's state produces tools nobody can reason about within a month, and debugging one means tracing an invisible graph. Declare the bindings, so the data flow is inspectable and a circular dependency is detectable. ## M4 · Who can press the dangerous button Permissions are enforced where the query runs, not where the button is drawn. ### Steps 1. Enforce at execution A hidden button is a convenience, not a permission. Files: `src/permissions.ts`, `tests/permission-bypass.test.ts` 2. Separate viewing from mutating Most people should see the tool and press nothing dangerous. Files: `src/permissions.ts` 3. Default to inaccessible An unconfigured tool should be closed, not open. Files: `src/tools.ts` ### Done when - [ ] A direct request without permission is refused - [ ] Viewing and mutating permissions are separate - [ ] Permission changes apply immediately - [ ] Unconfigured tools are inaccessible ### Watch out - Hiding a button from users without the permission, while the endpoint still accepts the request, means anyone who inspects the page can run the query anyway, and in an internal tool that query might delete records. Enforce at execution and treat the hidden button as a convenience rather than as the control. ## M5 · Confirming and recording what changed (production only) Every mutating action states its effect first and is logged after. ### Steps 1. Count affected rows before running The person pressing the button cannot otherwise know. Files: `src/preview.ts` 2. Log every mutating run Internal tools accumulate buttons nobody remembers adding. Files: `src/action-log.ts` 3. Alert on unusually large effects Before by threshold, and after by comparison to the usual. Files: `src/alerts.ts` ### Done when - [ ] Mutating queries preview affected rows - [ ] Large effects require an extra acknowledgement - [ ] Every mutating run is logged with actor and parameters - [ ] Unusually large runs alert ### Watch out - A mutating query with a mistaken parameter can update every row in a table, and the person pressing the button has no way to know until afterwards. Count the affected rows first, show that number in the confirmation, and refuse without confirmation for anything above a threshold.
# Operations · Appsmith Cloud ## Backup The builder's own state holds tool definitions and the action log, both of which matter. The target data belongs to whoever owns that database and is their responsibility, which is worth being explicit about. ## Restore Restore and confirm datasource credentials were not restored from an old backup with wider scope than the current ones, since a widened scope after a restore is invisible until something writes. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Rows affected per mutating run, compared against that query's usual. A run affecting a thousand times more rows than normal is the shape of a mistaken parameter and is visible immediately. ## Incident checklist A tool that updated more rows than intended is the incident, and the action log with parameters and counts is what turns recovery into a targeted fix rather than a restore from backup. 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 - [ ] A crafted parameter is confirmed unable to alter query structure - [ ] Read-only credentials are confirmed unable to write - [ ] A direct request without permission is confirmed refused - [ ] Mutating queries are confirmed to preview affected rows - [ ] Every mutating run is confirmed logged with its actor ## Launch constraint Do not market omitted Appsmith 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. The builder's own state. Separate from any datasource it connects to. DATABASE_URL=postgres://localhost:5432/builder # Required · secret. Scoped per datasource at the database. Never returned by any API response. DATASOURCE_CREDENTIALS=a secrets reference # Required. Applied to every query and not removable at runtime. DEFAULT_ROW_LIMIT=1000 # Required. Above this a mutating run needs an extra acknowledgement. MUTATION_CONFIRM_THRESHOLD=50 # Required. Present as a constant. It is the vulnerability this category is prone to. ALLOW_STRING_INTERPOLATION=never # Required. Who changed what in production, kept long enough to answer the question later. ACTION_LOG_RETENTION_DAYS=730
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Appsmith Cloud because the builder is only useful when its runtime, connectors, permissions, and deployments remain boring for years. The recurring cost buys schema migration, auth, permissions, connector drift, queues, deployments, backups, and auditability, not just the visible interface.
xconnectors, enterprise permissions, workflows, support, and managed hosting
xhundreds of connectors
xpixel-perfect app design
xmobile app-store builds
xenterprise permissions and scale
Don't feel like building it? These folks already made it free.
all 6 free alternatives to Appsmith Cloud →· no votes, no pay-to-list · just what's real
Appsmith Cloud pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/workspace | — | Up to 5 cloud users; 5 workspaces; 3 Git repositories; unlimited apps |
| business | $15/user | — | Up to 99 users; unlimited workspaces, Git repositories, environments and apps |
| enterprise | $2500/workspace | — | Starts at 100 users; unlimited workspaces, Git repositories, environments and apps |
free tier5 cloud users; 5 workspaces; 3 Git repositories; unlimited apps
billingmonthly self-serve Business; Enterprise starts at 100 users and is generally contract/annual billed
hidden costsBusiness is per user and a user present at any point in a billing month is charged for the whole month; Enterprise has a 100-user/$2,500 monthly floor, while air-gapped, dedicated or managed-hosting services are add-ons
verified 2026-08-14 · source ↗
Vibecode Appsmith Cloud
Kinda. The core of Appsmith Cloud is buildable in a weekend with the prompt on this page, but there are real gaps: connectors, enterprise permissions, workflows, support, and managed hosting, hundreds of connectors. Read the honest list above before committing.
How much does Appsmith Cloud cost?
Appsmith Cloud costs about $15/month (Business, checked 2026-08-14), which is $180 per year.
What do I lose by replacing Appsmith Cloud?
Honestly: connectors, enterprise permissions, workflows, support, and managed hosting; hundreds of connectors; pixel-perfect app design; mobile app-store builds; enterprise permissions and scale. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Appsmith Cloud?
Yes: Budibase (Internal apps, forms, automations, and portals in one box; much closer to Knack than its developer branding suggests.) Lowcoder (A self-hosted internal-tool canvas with a broad widget set; less polished, but not a toy.) Appsmith Free (The same hosted builder is free for five users, five workspaces, public apps, Git, Google sign-in, and standard roles.) All 6 curated free alternatives are at vibecodeit.com/appsmith-cloud/alternatives. The prompt is for when you want it exactly your way.