Build Appsmith Cloud

KINDAreplaces $15/mosaves $180/yrback to the verdict

0%0 of 17 items done

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

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-daythe files for this build are in the project pack

Data accessParameterised queries only, stored as templates with typed parametersCredentialsPer-datasource, scoped to the minimum the tools needWidgetsA fixed library bound to query resultsPermissionsPer-tool and per-action, enforced server sideRuntimeTypeScript with PostgreSQL for its own state

Before step 1

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

  1. installfree

    Why Runs the builder and the query execution layer.

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

    Verify node --version prints v22 or higher

  2. installfree 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. open ↗

    Verify Two databases exist and the builder cannot reach the target with its own credentials

  3. 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

  4. have readyfree

    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

  5. decidefree

    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

Data model

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

- 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.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/builderThe builder's own state. Separate from any datasource it connects to.
DATASOURCE_CREDENTIALSsecretrequireda secrets referenceScoped per datasource at the database. Never returned by any API response.
DEFAULT_ROW_LIMITrequired1000Applied to every query and not removable at runtime.
MUTATION_CONFIRM_THRESHOLDrequired50Above this a mutating run needs an extra acknowledgement.
ALLOW_STRING_INTERPOLATIONrequiredneverPresent as a constant. It is the vulnerability this category is prone to.
ACTION_LOG_RETENTION_DAYSrequired730Who changed what in production, kept long enough to answer the question later.

The build, in order

  1. Queries that cannot be assembled from input

    A parameter can never become part of the query's structure.

    1. Structure from a fixed list, never from input.

      Files src/queries.tstests/injection.test.ts

    2. Before they exist, not when they are run.

      Files src/validate.ts

    3. An unbounded query against production is an outage.

      Files src/queries.ts

    done when · tick each as it passes
    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.
  2. Credentials that can only do what is needed

    A read-only tool uses credentials that cannot write.

    1. Attempt a write and expect the database to refuse it.

      Files src/datasources.tstests/readonly.test.ts

    2. At save, so the mistake cannot be made.

      Files src/queries.ts

    3. Asserted in the suite across every response shape.

      Files tests/no-credential-leak.test.ts

    done when · tick each as it passes
    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.
  3. An interface composed from results

    Widgets bind to query results and update predictably.

    1. Implicit reactivity produces tools nobody can debug.

      Files src/bindings.ts

    2. Three states that all look like a blank widget otherwise.

      Files src/widgets.ts

    3. A cycle at runtime is a hang rather than an error.

      Files src/bindings.tssrc/bindings.test.ts

    done when · tick each as it passes
    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.
  4. Who can press the dangerous button

    Permissions are enforced where the query runs, not where the button is drawn.

    1. A hidden button is a convenience, not a permission.

      Files src/permissions.tstests/permission-bypass.test.ts

    2. Most people should see the tool and press nothing dangerous.

      Files src/permissions.ts

    3. An unconfigured tool should be closed, not open.

      Files src/tools.ts

    done when · tick each as it passes
    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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more no-code apps.