Skip to content

Database Studio

Every project with a managed database gets a Database Studio in the dashboard: connection details, a table browser with inline row editing, a SQL console, migrations history, a usage panel, and backups. Open your project, switch to the database section, then choose Production, Preview, or Dev. The selector is stored in the URL and every read or mutation is scoped to that channel.

Switching channels resets table selection, pagination, editor results, credentials, and operation state so stale Production state cannot issue a write against Preview (or the reverse). Saved SQL snippets remain shared, but local query history is stored per project and channel.

Connection

The Connection card reveals your standing connection URL (host, port, database, user, password) for the selected channel and lets you rotate credentials. apco dev --db always uses the Dev selection.

Rotating breaks that channel's running deployment

The old password stops working the moment rotation completes — there is no grace period. Redeploy/restart that channel afterward to pick up the new DATABASE_URL. Other channels are unchanged. See Credential rotation.

Table browser

Pick a table from the list (it shows an approximate row count and on-disk size) to browse it in two tabs:

Data — a paginated grid (50 rows/page):

  • Click a column header to sort by it (click again to reverse, a third time to clear).
  • The header shows the approximate total; click Count rows for an exact count(*) (can be slow on large tables, so it's opt-in).
  • Insert row opens a form built from the table's columns.
  • Rows with a primary key get edit and delete actions; tables without one show data read-only ("Editing and deleting rows requires a primary key"). Delete asks for confirmation and previews the DELETE statement it will run.

Structure — every column with its data type, nullability, default value, and primary key badge, plus the table's indexes and foreign keys (column → referenced_table(column)).

Views appear in a separate list (read-only — the row API only supports base tables); opening one hands SELECT * FROM <view> LIMIT 100 to the SQL console.

Row counts are estimates

Approximate counts come from the selected engine's catalog (pg_class for Postgres or information_schema.TABLES for MariaDB), not COUNT(*). They are fast but may lag; use Count rows for the exact number.

SQL console

Run arbitrary SQL against your database, straight from the browser, with schema-aware autocomplete (table and column names) as you type.

  • Type a query and hit Run (or ⌘/Ctrl + Enter).
  • Dry run on Postgres executes SQL inside a transaction that's always rolled back. It is intentionally disabled on MariaDB because DDL and other statements may commit implicitly; use an isolated Preview or Dev branch for rehearsal.
  • EXPLAIN runs EXPLAIN on the current statement.
  • Results render as a grid with row count, query duration, and (in dry-run mode) a rolled back badge.
  • Results are capped at 500 rows — larger result sets show a "truncated to 500" badge (the full rowCount is still reported).
  • Export the result grid as Copy CSV / JSON / Markdown or Download CSV.
  • Safe errors from the selected engine (syntax, constraints, permissions, ...) are shown directly.
  • Running a DROP, TRUNCATE, ALTER, or a DELETE/UPDATE without a WHERE clause asks for confirmation first (dry runs skip this — nothing is persisted either way). This is a client-side UX guard, not a security boundary.
  • History remembers your last 25 queries in this browser (stored in localStorage, per project and channel — clearing site data clears it too).
  • Saved queries are named SQL snippets stored on the server, shared across your own sessions/devices — save the current query, or pick and delete from the dropdown.

A few practical notes:

  • Queries run as the selected channel's tenant role — the same role that channel's app uses. Its privileges are the sandbox: it can only touch that physical database. That also means anything your app can do (including DROP TABLE), the console can do.
  • Multi-statement SQL is allowed; the grid shows the last statement's result (a banner warns you when there's more than one).
  • A single submission is limited to 100 KB of SQL.
sql
SELECT id, text, done, created_at
FROM todos
ORDER BY created_at DESC
LIMIT 50;

Migrations

The Migrations card reads your app's own _apco_migrations table (the convention table apco db migrate and @apco/db's runMigrations write to) — a quick way to confirm what's actually landed on the cloud database. Empty until your app has run at least one migration; see Migrations in the database guide.

Usage

Live size, connections, and history for the database:

  • A storage bar — bytes used vs. your plan's size limit.
  • A "storage limit exceeded" banner when the database is read-only for being over quota — see Over quota (read-only).
  • Active connections vs. your plan's connection limit.
  • A size-history sparkline once at least two nightly samples exist.

Backups

Gzipped SQL dumps of the selected channel database, stored by APCO Cloud:

  • Create backup queues an engine-native pg_dump or mariadb-dump; the row polls itself from pendingrunningcomplete/failed.
  • Only one backup can be in flight per project across all channels — the button surfaces BACKUP_IN_PROGRESS if you already have one running, and QUOTA_EXCEEDED once you're at your plan's backup limit (delete an old one first).
  • Download a completed backup as a .sql.gz file; delete removes it from the list (a finished backup only — a running one can't be deleted).
  • Retained backups remain listable/downloadable even when the selected branch has been deleted or is not ready; only creating a new backup requires ready.
  • There is no in-platform restore — see Backups in the database guide for the local restore command.

For scripts and agents

Every branch-bound route below accepts ?channel=production|preview|dev and defaults to Production. Every panel is backed by an API route you can call directly (see the HTTP API reference for full request/response shapes):

RouteWhatScope
POST /projects/:id/database/queryRun SQL, optional {rollback: true} dry run → {columns, rows, rowCount, truncated, durationMs, rolledBack}database:query or transitional projects:write
GET /projects/:id/database/schemaIntrospect: {tables: [{name, approxRowCount, sizeBytes, columns, indexes, foreignKeys}], views}projects:read
GET/POST/PATCH/DELETE /projects/:id/database/tables/:table/rowsPaginated browse / insert / update / delete a rowprojects:read / projects:write
GET /projects/:id/database/tables/:table/countExact count(*)projects:read
GET /projects/:id/database/migrationsApplied-migrations historyprojects:read
GET /projects/:id/database/usageLive size, read-only state, connections, size historyprojects:read
GET/POST /projects/:id/database/saved-queries, DELETE .../saved-queries/:queryIdList / save / remove saved SQL snippetsprojects:read / projects:write
GET/POST /projects/:id/database/backups, metadata/download/delete on :backupIdList / request / inspect / delete / download backups; ?allChannels=true lists every sourceprojects:read / projects:write
POST /projects/:id/database/rotate-credentialsRotate the tenant DB passwordprojects:write

All are Pro-gated like the database itself, except deleting a saved query and the backup list/delete/download routes — those stay available on a downgraded plan so you can retrieve and clean up your own data. SQL errors come back as HTTP 400 with code QUERY_ERROR; while the database is provisioning or not ready you get a 409. None of these routes ever return the connection URL or password.

APCO Cloud — ship apps with one command.