PandaStack

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/pandastack

TypeScript

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_url

Create 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):

FieldDefaultNotes
labelHuman-friendly tag surfaced in list/get.
cpu2vCPU for the database VM.
memory_mb1024RAM 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.

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):

RoutePurpose
POST /v1/queryRun SQL that returns rows. Body: {database, sql, params?, timeout_ms?, max_rows?}.
POST /v1/execRun a statement that returns no rows. Returns {rows_affected, duration_ms}.
POST /v1/transactionRun a batch of statements atomically: {database, statements:[{sql, params?}], timeout_ms?}.
GET /v1/healthLiveness (no auth): {status, postgres, ts}.
GET /v1/infoPostgreSQL version, installed extensions, broker version.
GET /v1/databasesList databases in the cluster (name, encoding, size).
POST /v1/databasesCreate 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)    # irreversible

TypeScript

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>/logs

Lifecycle

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 psql may 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 to gs://<bucket>/db/<id>/wal/.
  • Daily base backups. A self-contained pg_basebackup runs every 24h to gs://<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 sandbox

TypeScript

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_BUCKET configured)
  • 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 id

Rules 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 embedded
const 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)

LimitValue
EnginePostgreSQL 16
Sizes1g (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 poolingPgBouncer (transaction mode) — up to 500 client connections
StorageDurable per-DB volume (host-local in beta)
Idle behaviorCompute 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 encryptionTLS required (sslmode=require)
BackupsAutomatic — continuous WAL archiving + daily base backups (on managed deployments)
RecoveryFailover to latest backup (~3 min RTO) · clone / point-in-time restore to a new database
Extensionspgvector, 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.

On this page