Browse docs All docs
Docs / Read your app's data — schema and SQL
Guidedeploymill://guides/database/access

Read your app's data: schema and SQL

Once an app has a managed database, you can inspect and read its data through the agent surface, without ever putting the connection string in front of the agent. This guide covers the two direct-data-access tools and how they fit together:

  • describe_database: the schema (shape only, never row values). Free, all plans.
  • query_database: a single read-only SQL query, capped and audited. Paid.

Both work on a prod app or a preview app (a branched preview carries its own managed-database record). Both are also available on the REST /api/v1 surface (paid plans — REST API access is a paid capability).

Heads-up on previews: a clone-mode preview's branched database contains real production rows copied at branch time. Reading it reads prod-shaped data, so treat it accordingly.

Inspect the shape: describe_database

Start here. describe_database returns the schema only: tables, columns (name, type, nullability), primary keys, indexes, and an approximate row count per table. It never returns a single row of data, so it's free on every plan (including the Explore free tier).

describe_database({ applicationId })
{
  "ok": true,
  "applicationId": "app_123",
  "databaseManaged": true,
  "provider": "postgres",
  "schema": {
    "tables": [
      {
        "name": "todos",
        "columns": [
          { "name": "id", "dataType": "uuid", "nullable": false },
          { "name": "title", "dataType": "text", "nullable": false }
        ],
        "primaryKey": ["id"],
        "indexes": [{ "name": "todos_pkey", "columns": ["id"], "unique": true }],
        "approxRowCount": 42
      }
    ]
  }
}

Recoverable errors (success channel)

errorCodeMeaning
no_managed_databaseThe app has no DeployMill-managed database. Add a database block to .deploymill/project.json and reconcile_project.
describe_unsupportedThe backend can't introspect the per-app database from here (e.g. Supabase). Use that provider's console.

Run a read-only query: query_database

query_database runs one read-only SQL statement and returns capped rows. This is the agent's actual row-data surface. It requires a plan with direct data access (a paid Builder/Enterprise capability, see Upgrading).

query_database({ applicationId, sql: "select id, title from todos order by created_at desc", maxRows: 100 })
{
  "ok": true,
  "applicationId": "app_123",
  "provider": "postgres",
  "columns": ["id", "title"],
  "rows": [{ "id": "…", "title": "ship it" }],
  "rowCount": 1,
  "truncated": false
}
  • Single statement only: SELECT/WITH/EXPLAIN/SHOW/TABLE/VALUES. No

    ;-separated batches, no DML/DDL.

  • maxRows defaults to 1000 and is hard-capped at 1000. When more rows match,

    truncated: true, so narrow the query or paginate in SQL.

  • Read-only is enforced two ways: a parse guard rejects obviously mutating or

    multi-statement SQL up front, and the query runs inside a read-only transaction with a server-enforced statement timeout. A write that slips past the guard is still rejected by the database.

  • Secret-named columns are redacted: cells in columns whose name contains

    password, secret, token, api_key, private_key, access_key, or client_secret come back as [redacted].

  • Every call is audited (the SQL text and row count, never the row data).

Recoverable errors (success channel)

errorCodeMeaning
no_managed_databaseThe app has no managed database.
not_read_onlyThe SQL was rejected by the parse guard (mutating or multi-statement).
query_unsupportedThe backend has no clean read-only SQL path (e.g. Supabase).
query_failedThe database rejected the query (syntax error, write blocked by the read-only transaction, timeout, …).
upgrade_requiredThe org's plan doesn't include direct data access. Includes an upgradeGuide pointer (and upgradeUrl when configured).
data_access_disabledAn org admin turned the direct-data-access governance toggle off, even on a paid plan.

A typical flow

  1. describe_database to learn the tables and columns (free; do this first).
  2. query_database to read the rows you need (read-only, capped, audited).

Security notes

  • The app's real DATABASE_URL is the canonical "secret the agent never sees".

    None of these tools return it.

  • query_database is gated by the direct data access entitlement and can be

    turned off org-wide by an admin governance toggle. It returns coded upgrade_required / data_access_disabled rather than a silent dead-end.

  • Reads are attributable: every query_database call lands in the audit trail.