Appearance
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
DELETEstatement 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
EXPLAINon 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
rowCountis 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 aDELETE/UPDATEwithout aWHEREclause 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_dumpormariadb-dump; the row polls itself frompending→running→complete/failed. - Only one backup can be in flight per project across all channels — the button surfaces
BACKUP_IN_PROGRESSif you already have one running, andQUOTA_EXCEEDEDonce you're at your plan's backup limit (delete an old one first). - Download a completed backup as a
.sql.gzfile; 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):
| Route | What | Scope |
|---|---|---|
POST /projects/:id/database/query | Run SQL, optional {rollback: true} dry run → {columns, rows, rowCount, truncated, durationMs, rolledBack} | database:query or transitional projects:write |
GET /projects/:id/database/schema | Introspect: {tables: [{name, approxRowCount, sizeBytes, columns, indexes, foreignKeys}], views} | projects:read |
GET/POST/PATCH/DELETE /projects/:id/database/tables/:table/rows | Paginated browse / insert / update / delete a row | projects:read / projects:write |
GET /projects/:id/database/tables/:table/count | Exact count(*) | projects:read |
GET /projects/:id/database/migrations | Applied-migrations history | projects:read |
GET /projects/:id/database/usage | Live size, read-only state, connections, size history | projects:read |
GET/POST /projects/:id/database/saved-queries, DELETE .../saved-queries/:queryId | List / save / remove saved SQL snippets | projects:read / projects:write |
GET/POST /projects/:id/database/backups, metadata/download/delete on :backupId | List / request / inspect / delete / download backups; ?allChannels=true lists every source | projects:read / projects:write |
POST /projects/:id/database/rotate-credentials | Rotate the tenant DB password | projects: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.
Related
- Managed databases — engines, enabling, migrations, backups, quotas
- Hybrid dev — local dev with the cloud database
- HTTP API reference