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.
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.
vetta_migrations table the platform keeps
on the app’s own database:
- new name → the SQL runs and the response is
201with the ledger row; - same name, same SQL → nothing runs and the response is
200with the existing row — a replay; - same name, different SQL →
409 version_conflict; a migration’s SQL is immutable, so write a new one.
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[] }.