Skip to main content
Every fullstack app comes with a managed Postgres database. These routes reach it directly — no connection string, no client library — so an agent, the SDK or the CLI can work with data the way they work with any other resource. All routes live under one app: /v1/apps/{id}/db/…. {id} is an app_ id, an app name, or the word default, which resolves the organization’s only fullstack app — the standalone feeling for the common case of one app. With two or more fullstack apps default answers 409 ambiguous_app, with the candidate ids in the message; with none, 404 not_found. Every answer carries the resolved app in a vetta-app-id response header: a script that creates a second app later pins the one it meant by passing that id instead of default. The database exists only once the app is active: a frontend_only app answers 501 feature_not_configured, and a still-provisioning backend answers a retryable job_not_ready.

Run SQL

POST /v1/apps/{id}/db/query — scope agents:write
string
required
The SQL to run.

List tables

GET /v1/apps/{id}/db/tables — scope agents:read Every user table in the database, including the vetta_migrations ledger once a migration has run. Not paginated: one database has a bounded catalogue.
rows and size_bytes are estimates from the database’s statistics and may be null on a table that has never been analysed.

REST

A passthrough to the database’s REST layer: every table is a resource under …/db/rest/, filters and ordering ride the query string, and JSON goes in and out. /v1/apps/{id}/db/rest/todos?done=eq.false&order=id reads open todos; POST …/rest/todos with a JSON body inserts; PATCH …/rest/todos?id=eq.7 updates; DELETE …/rest/todos?id=eq.7 deletes. Filters are column=op.value: eq, neq, gt, gte, lt, lte, like, ilike, is, in, and their not. negations. Three request headers are forwarded — Prefer (return=representation to get the written rows back, count=exact for a total), Range and Content-Type — and the upstream status and JSON body come back verbatim, with the Content-Range header when the upstream set it.
This is admin access. Calls run as the database’s service role, so the row-level security policies your app enforces for its own users do not apply here. Every call — read or write — is recorded in the audit log as app.db_rest with its method, path and status; the body is never recorded.

Apply a migration

POST /v1/apps/{id}/db/migrations — scope agents:write. Idempotency-Key is required.
string
required
Lowercase letters, digits, _ and -; up to 128 characters. The name is what makes the migration idempotent.
string
required
The SQL to run. It is hashed (SHA-256) and recorded alongside the name.
A migration is applied once per name, tracked in a vetta_migrations table the platform keeps on the app’s own database:
  • new name → the SQL runs and the response is 201 with the ledger row;
  • same name, same SQL → nothing runs and the response is 200 with the existing row — a replay;
  • same name, different SQL → 409 version_conflict; a migration’s SQL is immutable, so write a new one.
A newly applied migration is recorded in the audit log as app.db_migrated and pushed to webhook subscribers as app.db.migrated with { app_id, migration_id, name }; a replay is neither.

List migrations

GET /v1/apps/{id}/db/migrations — scope agents:read The ledger, in applied_at order, as { "data": db_migration[] }.

Errors