Databases
Managed PostgreSQL 16 running inside dedicated Firecracker microVMs — a real, durable database provisioned in seconds.
Beta. Managed Databases are in public beta. The API is stable for create / list / get / delete, but advanced features (branching, read replicas, point-in-time restore) are still in progress — see Coming soon. Don't yet rely on a single beta database as the only copy of irreplaceable production data.
A database is a managed PostgreSQL 16 instance running in its own dedicated Firecracker microVM — not a shared schema on a multi-tenant cluster. Each database gets its own kernel, its own postgres process, a connection pooler, and a durable data volume. You get a native postgres:// connection string in one API call.
Unlike ordinary sandboxes, databases are persistent: the TTL reaper that recycles ordinary sandboxes never deletes them, and only an explicit DELETE destroys the data. They are, however, ephemeral-friendly — an idle database scales to zero (hibernates) and wakes on the next connection, so a throwaway per-PR or on-the-go database costs compute only while it's in use. See Lifecycle and the Ephemeral environments guide for when to reach for one.
Create a database
REST
curl -X POST https://api.pandastack.ai/v1/databases \
-H "Authorization: Bearer $PANDASTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label":"my-app-db","size":"1g"}'size picks the RAM tier — 1g (default, 2 vCPU), 4g (2 vCPU), or 16g
(4 vCPU). Memory is fixed at create time (a snapshot-restored microVM cannot
be resized in place); to change it later, clone the database
into a new size.
Python
import pandastack
client = pandastack.Client(api_key="pds_...")
db = client.databases.create(label="my-app-db")
print(db["connection_url"])
# postgres://pandastack:<pw>@<id>.db.pandastack.ai:5432/pandastackTypeScript
import { Client } from "@pandastack/sdk";
const client = new Client({ apiKey: "pds_..." });
const db = await client.databases.create({ label: "my-app-db" });
console.log(db.connection_url);CLI
pandastack db create --label my-app-db
# → prints id + connection_urlCreate provisions a fresh PostgreSQL VM and waits for it to publish credentials — typically 60–180s end to end (VM boot ~30–60s, then PostgreSQL bootstrap). If PostgreSQL hasn't come up within the wait window, the call still returns with "status": "provisioning" and an error hint instead of failing — poll GET /v1/databases/{id} until status is running and the credentials appear. On success the response includes everything you need to connect:
{
"id": "3f95041e-0ba0-4068-8302-512e811edd60",
"status": "running",
"template": "postgres-16",
"label": "my-app-db",
"host": "3f95041e-0ba0-4068-8302-512e811edd60.db.pandastack.ai",
"port": 5432,
"database": "pandastack",
"username": "pandastack",
"password": "nUf0NrDb4zm_gQXBKhVRn-b77RKllFLq",
"connection_url": "postgres://pandastack:[email protected]:5432/pandastack",
"broker_token": "pds_pg_M6rRbwNPp0Ed14PBEsTxw8ksDcX2jt7F",
"broker_url": "https://api.pandastack.ai/v1/databases/3f95041e-.../proxy"
}The password is only returned at creation and from GET /v1/databases/{id} while the database is running. Store the connection string in your secret manager — there is no "reset password" endpoint in beta.
Create fields (all optional):
| Field | Default | Notes |
|---|---|---|
label | — | Human-friendly tag surfaced in list/get. |
cpu | 2 | vCPU for the database VM. |
memory_mb | 1024 | RAM for the database VM. |
cpu/memory_mb set the VM's requested size, but the baked postgres-16 snapshot governs the actual guest RAM — Firecracker can't change a snapshot's memory at restore. For larger databases, build a custom postgres-16-based template baked at the size you need. PgBouncer is tuned for the default footprint.
Connect
There are two ways to talk to your database.
1. Native postgres:// (recommended)
Use the connection_url directly with any PostgreSQL client or driver. Traffic is routed to your VM by SNI on <id>.db.pandastack.ai — TLS is required.
psql "postgres://pandastack:<pw>@<id>.db.pandastack.ai:5432/pandastack?sslmode=require"import psycopg
with psycopg.connect(db["connection_url"]) as conn:
conn.execute("CREATE TABLE IF NOT EXISTS t (id serial, name text)")
conn.execute("INSERT INTO t (name) VALUES (%s)", ["hello"])
print(conn.execute("SELECT count(*) FROM t").fetchone())Connections go through PgBouncer in transaction-pooling mode — a good fit for serverless and many short-lived agent connections (it accepts up to 500 client connections and multiplexes them onto a small PostgreSQL pool). Because pooling is transaction-scoped, session-level features (e.g. SET that must persist across statements, LISTEN/NOTIFY, advisory-lock sessions, prepared statements across transactions) may not behave as on a direct connection — keep state within a single transaction.
2. REST query broker (HTTP)
For environments where you can't open a raw TCP connection (edge functions, browsers via a backend), each database exposes an HTTP query broker. The broker lives under the broker_url from your create response — query at <broker_url>/v1/query. Authenticate with the broker_token you got at creation, and pass the database name in the body (use "pandastack", the database created for you — it's also the database field in your create response):
# $BROKER_URL is the broker_url from your create response, e.g.
# https://api.pandastack.ai/v1/databases/<id>/proxy
curl -X POST "$BROKER_URL/v1/query" \
-H "Authorization: Bearer $BROKER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"database":"pandastack","sql":"SELECT now()"}'A successful response returns the result set:
{
"columns": [{ "name": "now", "type": "TIMESTAMPTZ" }],
"rows": [["2026-06-13T00:00:00Z"]],
"count": 1,
"duration_ms": 6
}Both database and sql are required. Omitting database returns {"error":"database required"} (HTTP 400); the default database provisioned with every managed instance is named pandastack.
All broker routes live under the same broker_url base and use the same Bearer broker_token (except /v1/health, which is unauthenticated):
| Route | Purpose |
|---|---|
POST /v1/query | Run SQL that returns rows. Body: {database, sql, params?, timeout_ms?, max_rows?}. |
POST /v1/exec | Run a statement that returns no rows. Returns {rows_affected, duration_ms}. |
POST /v1/transaction | Run a batch of statements atomically: {database, statements:[{sql, params?}], timeout_ms?}. |
GET /v1/health | Liveness (no auth): {status, postgres, ts}. |
GET /v1/info | PostgreSQL version, installed extensions, broker version. |
GET /v1/databases | List databases in the cluster (name, encoding, size). |
POST /v1/databases | Create a database: {name, owner?}. |
DELETE /v1/databases/{name} | Drop a database. |
Broker defaults: up to 10,000 rows per query, 30s query timeout (60s for transactions, max 100 statements), and a 1 MiB max SQL size — all overridable per request via max_rows/timeout_ms. The broker connects to PostgreSQL through PgBouncer, so it shares the pooled connection budget.
List, get, delete
REST
curl -H "Authorization: Bearer $PANDASTACK_API_KEY" https://api.pandastack.ai/v1/databases
curl -H "Authorization: Bearer $PANDASTACK_API_KEY" https://api.pandastack.ai/v1/databases/<id>
curl -X DELETE -H "Authorization: Bearer $PANDASTACK_API_KEY" https://api.pandastack.ai/v1/databases/<id>Python
client.databases.list() # [{id, status, label, created_at}, ...]
client.databases.get(db_id) # full record incl. connection_url
client.databases.delete(db_id) # irreversibleTypeScript
await client.databases.list();
await client.databases.get(id);
await client.databases.delete(id);list returns metadata only (no credentials). Call get to retrieve the connection string for a specific database.
DELETE is irreversible. It destroys the VM and its data volume, and automatically deletes any GCS backup archives (WAL segments and base backups). Export anything you need with pg_dump first.
Connection, stats, and logs
Three read endpoints complement the core CRUD:
# Connection details only (host/port/database/username/password/connection_url)
curl -H "Authorization: Bearer $PANDASTACK_API_KEY" \
https://api.pandastack.ai/v1/databases/<id>/connection
# Live PostgreSQL stats (connection counts, database size, activity)
curl -H "Authorization: Bearer $PANDASTACK_API_KEY" \
https://api.pandastack.ai/v1/databases/<id>/stats
# Tail the PostgreSQL server log
curl -H "Authorization: Bearer $PANDASTACK_API_KEY" \
https://api.pandastack.ai/v1/databases/<id>/logsLifecycle
A database moves through the same states as any persistent environment — see Ephemeral environments for the shared model and a side-by-side comparison with apps.
create ──► running ──► (idle) ──► hibernated ──► (connect) ──► running ──► delete- create provisions a fresh Postgres VM and waits for credentials (60–180s).
- running accepts connections on
<id>.db.pandastack.ai:5432. - hibernated (scale-to-zero): after an idle window with no live connection, the database VM is snapshotted and stopped — this frees CPU/RAM and stops active-compute billing. The durable data volume is untouched. A database with an open connection (e.g. a pooler) is never hibernated; the idle countdown only starts after the last connection closes.
- wake-on-connect: the next connection through the proxy transparently restores the VM (~1–2s) and proceeds. Pooled clients/ORMs that retry absorb this; a single bare
psqlmay need one reconnect. - delete is the only thing that destroys data (VM + volume + GCS backups).
Scale-to-zero is what makes a throwaway per-PR or on-the-go database economical — you pay compute only for the minutes it's actually queried, plus storage while it sleeps. For an always-awake database, keep a connection (e.g. a pooler) open. See when to use an ephemeral environment.
Persistence and durability
- Databases are created with
persistent: true. The TTL reaper that recycles ordinary sandboxes skips databases — they hibernate when idle (see Lifecycle) but are never auto-deleted; they keep their data until you delete them. - Data survives hibernate, wake, and host reboot. It lives on a dedicated volume attached to the database VM, separate from the ephemeral rootfs — a memory-snapshot failure or a hibernate/wake cycle never touches it.
- The database volume counts against your workspace's plan storage quota — same meter as persistent volumes. Storage within the quota is free; provisioned storage beyond it bills at $0.15 per GiB-month. There is no separate database storage rate.
- In beta, a database is pinned to the agent host it was created on (same model as host-local volumes). Cross-host durability (GCP Persistent Disk + scheduler affinity) is on the roadmap.
Backups and recovery
On managed deployments (where PANDASTACK_SNAPSHOT_BUCKET is configured), every database is continuously and automatically protected — no setup required:
- Continuous WAL archiving. Each write-ahead-log segment is shipped off the host as PostgreSQL fills it (via
archive_command), spooled, and uploaded togs://<bucket>/db/<id>/wal/. - Daily base backups. A self-contained
pg_basebackupruns every 24h togs://<bucket>/db/<id>/base/, so a restore never depends on WAL older than the latest base backup.
This means a failed database can be rebuilt from object storage on another host — you are not relying on the single VM's disk. (pg_dump is still useful for portable, point-in-time exports you control, but it is not the backup mechanism — archiving is automatic.)
Failover
If a database enters a failed state (agent host crash, kernel panic), restore it onto a healthy host from the latest GCS backup. Failover requires a multi-node deployment — on a single-node deployment the endpoint returns 501 Not Implemented (there's no second host to move to).
REST
curl -X POST https://api.pandastack.ai/v1/databases/<id>/failover \
-H "Authorization: Bearer $PANDASTACK_API_KEY"Python
restored_db = client.databases.failover(db_id)
print(restored_db["connection_url"]) # same ID, new sandboxTypeScript
const restoredDb = await client.databases.failover(id);
console.log(restoredDb.connection_url);The database retains the same ID and connection URL — only the underlying sandbox changes. Expected recovery time is ~3 minutes (base backup download + WAL replay).
Failover availability
Before calling failover(), check whether a restore is possible by inspecting the failover_available, failover_reason, and failover_eta_seconds fields in the GET /databases/{id} response (only populated when status="failed").
Failover is available when:
- The database status is
failed - GCS archiving is enabled (
PANDASTACK_SNAPSHOT_BUCKETconfigured) - At least one healthy agent is available in the multi-node deployment
- A base backup archive exists in
gs://<bucket>/db/<id>/base/
If failover is unavailable (e.g., no GCS archive, single-node deployment), the failover_reason field explains why.
Clone and point-in-time restore
Clone any database into a brand-new database built from its backups. The
source is never touched — cloning works against running, hibernated, and even
failed databases — and the clone gets fresh credentials plus its own
independent backup stream.
REST
# Clone the latest backed-up state
curl -X POST https://api.pandastack.ai/v1/databases/<id>/clone \
-H "Authorization: Bearer $PANDASTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label":"my-db-copy"}'
# Point-in-time restore: stop WAL replay at an instant (RFC3339)
curl -X POST https://api.pandastack.ai/v1/databases/<id>/clone \
-H "Authorization: Bearer $PANDASTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label":"before-the-bad-migration","target_time":"2026-08-06T09:30:00Z"}'Python
# "Undo" a destructive change: restore to 5 minutes before it happened
clone = client.databases.clone(
db_id,
label="before-the-bad-migration",
target_time="2026-08-06T09:30:00Z",
)
print(clone["connection_url"]) # a NEW database, cloned_from = source id
# Resize path: clone into a bigger RAM tier
bigger = client.databases.clone(db_id, size="4g")TypeScript
const clone = await client.databases.clone(id, {
label: "before-the-bad-migration",
target_time: "2026-08-06T09:30:00Z",
});
console.log(clone.connection_url); // a NEW database, cloned_from = source idRules for target_time:
- Must be at least 2 minutes in the past — the WAL archive trails live writes by up to ~60 seconds.
- Must be no later than the source's last archived WAL. An idle database produces no new WAL, so the restorable window ends at its last write.
- Must be after the source's oldest base backup (the first one lands ~2 minutes after the database first becomes ready).
The API returns 202 with the new database id immediately; the clone
provisions in the background (base backup download + WAL replay — minutes for
large databases). Both SDKs wait for readiness by default. Deleting the
source is refused with 409 while one of its clones is still provisioning.
Reset credentials
Rotate the postgres password and the REST broker token of a running database in one call. The response carries the new, verified credentials; every client using the old ones is disconnected immediately. If the call errors, nothing is left half-rotated — retrying is safe.
curl -X POST https://api.pandastack.ai/v1/databases/<id>/reset-credentials \
-H "Authorization: Bearer $PANDASTACK_API_KEY"fresh = client.databases.reset_credentials(db_id)
print(fresh["connection_url"]) # new password embeddedconst fresh = await client.databases.resetCredentials(id);Idle auto-suspend
To keep costs low, a database with no active connections pauses its compute
after an idle period, and resumes automatically within a few seconds on the
next connection — no action required. Your data and storage are untouched;
you are not billed for compute while it is idle. The only visible effect is a
slightly slower first query after an idle stretch, exactly like a serverless
database. This applies to both native postgres:// and REST broker
connections.
If your workload is latency-sensitive and must never pay a resume delay, keep the database always on:
curl -X PATCH https://api.pandastack.ai/v1/databases/<id> \
-H "Authorization: Bearer $PANDASTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"always_on": true}'# At creation, or later via update:
db = client.databases.create(label="prod", always_on=True)
client.databases.update(db_id, always_on=True)await client.databases.update(id, { always_on: true });You can also wake a database explicitly (rarely needed — the next connection does it for you):
curl -X POST https://api.pandastack.ai/v1/databases/<id>/wake \
-H "Authorization: Bearer $PANDASTACK_API_KEY"Limits (beta)
| Limit | Value |
|---|---|
| Engine | PostgreSQL 16 |
| Sizes | 1g (2 vCPU · 1 GiB, default) · 4g (2 vCPU · 4 GiB) · 16g (4 vCPU · 16 GiB) — fixed at create; resize = clone into a new size |
| Connection pooling | PgBouncer (transaction mode) — up to 500 client connections |
| Storage | Durable per-DB volume (host-local in beta) |
| Idle behavior | Compute auto-suspends when idle; resumes in a few seconds on the next connection (always_on to disable). Storage always billed; compute billed only while active. |
| Connection encryption | TLS required (sslmode=require) |
| Backups | Automatic — continuous WAL archiving + daily base backups (on managed deployments) |
| Recovery | Failover to latest backup (~3 min RTO) · clone / point-in-time restore to a new database |
| Extensions | pgvector, pg_stat_statements, pg_trgm, ltree, hstore, uuid-ossp, pgcrypto, unaccent (pre-installed) |
Automatic cleanup on delete
When you delete a database, PandaStack automatically cleans up any GCS backup archives (WAL segments and base backups) stored in gs://<bucket>/db/<id>/. This ensures GDPR/data-privacy compliance — no orphaned sensitive data remains after a database is deleted.
Coming soon
These database features are in progress and not yet available:
- Branching — instant copy-on-write database branches for previews and tests (fork a database the way you fork a sandbox). Until then, clone covers the copy-a-database case from backups.
- Read replicas — scale reads horizontally.
- Storage autoscaling — volumes that grow on demand instead of a fixed size.
- Cross-host durability — databases that survive agent-host replacement (GCP PD + affinity).
- More engines — MySQL and Redis-compatible managed instances.
- Dashboard SQL console — run queries from the dashboard without a local client.
Want one of these sooner? Tell us in GitHub Discussions.
Volumes
Named, persistent ext4 block devices you can attach to sandboxes — for datasets, model weights, and durable workspaces.
Ephemeral environments
Apps and managed databases share one lifecycle — create, serve, scale-to-zero on idle, wake on demand, delete. What's guaranteed at each step, and when to reach for an ephemeral environment versus an always-on one.