Skip to main content
Every fullstack app has a managed Postgres database, and client.database reaches it without a connection string: SQL, the table catalogue, a table-level REST facade, and migrations. Four methods and five verbs on from(table). API detail: Database. Every method takes an optional { app } — an app_ id or an app name. Omit it when the organization has one fullstack app and that app is used. With several, the call fails with ambiguous_app and the message lists the candidates; with none, not_found.

query

POST /v1/apps/{app}/db/query. Arbitrary SQL, rows back as JSON.

tables

GET /v1/apps/{app}/db/tables. Each DbTable is { object: "db_table", name, schema, rows, size_bytes }; rows and size_bytes are estimates and may be null.

from

The verbs over /v1/apps/{app}/db/rest/{table}: GET, POST, PATCH, DELETE. Writes ask for the affected rows back (Prefer: return=representation), so each promise resolves to the rows the database saw — insert to what it inserted, update and delete to what they touched. upsert is a POST that merges duplicates instead of refusing them (Prefer: resolution=merge-duplicates): on the primary key by default, or on the unique columns onConflict names ("email", "org_id,slug" → ?on_conflict=). The REST layer’s own PUT (one row, every key column repeated in the filter) and HEAD (a count with no body) have no SDK verb: upsert covers what PUT does, a count is database.query("select count(*) from …"), and a client that already speaks that dialect calls the REST route directly. A DbFilter is column → value. A bare string, number or boolean means equality; null means is.null; an array means in.(…); and a string that begins with an operator is passed through as is — eq, neq, gt, gte, lt, lte, like, ilike, is, in, and not. in front of any of them. select’s third argument orders and pages: order is column, column.desc, or column.asc.nullslast; limit and offset are row counts.
from() runs as the database’s service role: the row-level security policies your app enforces for its own users do not apply. Every call is recorded in the audit log with its method and path.

migrate

POST /v1/apps/{app}/db/migrations, with the idempotency key the client mints for every write. A migration runs once per name: a repeat with the same SQL resolves to the existing row without running anything, and a repeat with different SQL under the same name rejects with version_conflict.

migrations

GET /v1/apps/{app}/db/migrations. Each DbMigration is { id, object: "db_migration", name, sha256, applied_at, statements }, in the order applied.