/ Docs

Databases API

Manage PostgreSQL databases.

Manage PostgreSQL databases.

List Databases

GET /projects/{projectId}/databases
Authorization: Bearer <platform_token>

Response:

{
  "data": [
    {
      "id": "abc-123-456-789",
      "database_name": "main_db",
      "status": "active",
      "connection_string": "postgresql://volcano_client_11111111-1111-1111-1111-111111111111:vpg_abc123@database.volcano.dev:5432/myapp_main?sslmode=require&application_name=volcano_full_access",
      "region": "aws-us-east-1",
      "pg_version": "16",
      "storage_bytes": 1610612736,
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    }
  ]
}

storage_bytes is the database's latest observed storage: its own on-disk size, plus what each of its branches has diverged from it, plus what its backups cost to hold. It is the figure your storage allowance is enforced against, and the stats endpoint breaks it down. A background pass records it, so it is absent until the database has been sampled and can trail the stats endpoint's current_storage_bytes, which measures on request.

Get Database

GET /projects/{projectId}/databases/{databaseName}
Authorization: Bearer <platform_token>

Response:

{
  "id": "abc-123-456-789",
  "database_name": "main_db",
  "status": "active",
  "connection_string": "postgresql://volcano_client_11111111-1111-1111-1111-111111111111:vpg_abc123@database.volcano.dev:5432/myapp_main?sslmode=require&application_name=volcano_full_access",
  "region": "aws-us-east-1",
  "pg_version": "16",
  "created_at": "2024-01-01T00:00:00Z",
  "updated_at": "2024-01-01T00:00:00Z"
}

Note: connection_string is only shown when status is active. Use this connection string to connect from your functions or applications. It contains Volcano-managed per-database client credentials (volcano_client_{database_id} with a vpg_ password); internal credentials are never returned and will not authenticate through pgproxy.

Create Database

POST /projects/{projectId}/databases
Authorization: Bearer <platform_token>
Content-Type: application/json

Request:

{
  "name": "main_db"
}

Response: 201 Created

Database name is normalized to PostgreSQL format (lowercase, underscores). Names can be up to 64 characters.

Limit: Each project can hold 1 database on HOBBY and up to 10,000 on SUPERAGENT. Creating a database over the plan's cap returns 403 Forbidden.

Delete Database

DELETE /projects/{projectId}/databases/{databaseName}
Authorization: Bearer <platform_token>

Response: 204 No Content

Permanently deletes the database. Cannot be undone.

Returns 409 while a restore is running on the database; wait for it to finish. If Volcano cannot tell whether one is running it returns 503 rather than delete a database that might be mid-restore — retry.

Reset Password

POST /projects/{projectId}/databases/{databaseName}/reset-password
Authorization: Bearer <platform_token>

Response:

{
  "message": "Password reset successful",
  "role_name": "volcano_client_11111111-1111-1111-1111-111111111111",
  "new_password": "vpg_new_secure_password",
  "connection_string": "postgresql://volcano_client_11111111-1111-1111-1111-111111111111:vpg_new_secure_password@database.volcano.dev:5432/mydb?sslmode=require&application_name=volcano_full_access"
}

Update your DATABASE_URL environment variable with the new connection string. The previous Volcano password stops opening new connections within a few seconds of the reset, and connections already open are not interrupted. Reset does not expose or rotate the internal owner password.

Returns 409 while a restore is running on the database — a restore rewrites the credentials on its way out — and 503 if Volcano cannot tell whether one is.

Branches

A branch is a copy-on-write fork of a database. See Branching for the concepts; this section is the endpoint reference.

List Branches

GET /projects/{projectId}/databases/{databaseName}/branches
Authorization: Bearer <platform_token>

Response:

{
  "data": [
    {
      "id": "9b1f0f4c-2b8e-4f43-9a71-1f6c2f0f2c31",
      "database_id": "abc-123-456-789",
      "project_id": "11111111-1111-1111-1111-111111111111",
      "name": "feature_checkout",
      "status": "active",
      "ttl_seconds": 86400,
      "expires_at": "2024-01-02T00:00:00Z",
      "storage_bytes": 536870912,
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:05Z"
    }
  ]
}

Every branch is listed, including those still provisioning and those that failed, since each still holds a name. Connection strings are omitted — fetch a single branch to get its connection string.

Create Branch

POST /projects/{projectId}/databases/{databaseName}/branches
Authorization: Bearer <platform_token>
Content-Type: application/json

Request:

{
  "name": "feature_checkout",
  "ttl_seconds": 86400
}

Response: 202 Accepted, with the branch in provisioning and no connection string. Poll Get Branch until it reports active.

ttl_seconds is between 3600 and 2592000 (one hour to 30 days) and defaults to seven days. Names are lowercase letters, numbers, and underscores, up to 64 characters, unique within the parent database.

StatusMeaning
400Invalid name or lifetime
403Branch allowance reached, or branching is not on this plan
404Project or database not found
409Name already exists, or the database cannot be branched right now
503Branching is temporarily unavailable

Limit: 10 branches per database on HOBBY, 25 on SUPERAGENT. Branches in every state count, including those still provisioning.

Get Branch

GET /projects/{projectId}/databases/{databaseName}/branches/{branchName}
Authorization: Bearer <platform_token>

Response:

{
  "id": "9b1f0f4c-2b8e-4f43-9a71-1f6c2f0f2c31",
  "database_id": "abc-123-456-789",
  "project_id": "11111111-1111-1111-1111-111111111111",
  "name": "feature_checkout",
  "status": "active",
  "connection_string": "postgresql://volcano_client_9b1f0f4c-2b8e-4f43-9a71-1f6c2f0f2c31:vpg_xyz789@database.volcano.dev:5432/main_db?sslmode=require&application_name=volcano_full_access",
  "ttl_seconds": 86400,
  "expires_at": "2024-01-02T00:00:00Z",
  "storage_bytes": 536870912,
  "created_at": "2024-01-01T00:00:00Z",
  "updated_at": "2024-01-01T00:00:05Z"
}

connection_string is present only while the branch is active. It carries the branch's own username and password; application_name selects the access mode exactly as it does for the parent. storage_bytes is the branch's divergence from its parent, not its apparent size.

Extend Branch

PATCH /projects/{projectId}/databases/{databaseName}/branches/{branchName}
Authorization: Bearer <platform_token>
Content-Type: application/json

Request:

{
  "ttl_seconds": 604800
}

Replaces the branch's lifetime and restarts the countdown from now. The new duration is remembered, so a later reset re-arms the same lifetime.

Reset Branch

POST /projects/{projectId}/databases/{databaseName}/branches/{branchName}/reset
Authorization: Bearer <platform_token>

Discards everything written on the branch and re-forks it from the parent as it is now. The branch keeps its name and connection string, and its lifetime is re-armed. It does not serve connections for the duration of the reset.

Returns 202 with the branch in provisioning; the rewind runs in the background. Poll the branch until it reports active before connecting again.

Returns 409 if the branch is not active, a reset is already in progress, or the parent database is being restored — a reset re-forks from the parent, so it waits for the restore and for the provider's cooldown afterwards.

Rotate Branch Password

POST /projects/{projectId}/databases/{databaseName}/branches/{branchName}/reset-password
Authorization: Bearer <platform_token>

Issues a new password and invalidates the previous connection string. Existing connections are not interrupted, and the previous password stops opening new ones within a few seconds. The parent database's credentials are untouched.

Delete Branch

DELETE /projects/{projectId}/databases/{databaseName}/branches/{branchName}
Authorization: Bearer <platform_token>

Response: 202 Accepted

{
  "status": "deleting",
  "message": "branch deletion in progress"
}

The branch stops accepting connections at once; its fork and its row are removed by a background job. Deleting a branch that is still provisioning is allowed and stops the build, and repeating the call while teardown is in progress is accepted again. Once the branch is gone the call returns 404.

Query a Branch

Every /query/* verb has a branch-targeted twin:

POST /databases/{databaseName}/branches/{branchName}/query/select
Authorization: Bearer <service_key or auth_user_access_token>
Content-Type: application/json

The request and response bodies are identical to the parent routes documented under Database Query API. The same tokens work — service keys and end-user tokens are project-scoped — and anonymous keys are rejected on a branch just as they are on a parent. A branch that does not exist answers 404, indistinguishable from a database that does not exist.

Backups

A backup is a copy of the database at a point in time. See Backups and restore for the concepts; this section is the endpoint reference. Backups cover the database itself — branches are neither backed up nor restored.

Every endpoint in this section is SUPERAGENT-only. On the HOBBY plan they all answer 403, reads included.

List Backups

GET /projects/{projectId}/databases/{databaseName}/backups
Authorization: Bearer <platform_token>

Response:

{
  "data": [
    {
      "name": "before_migration",
      "source": "manual",
      "size_bytes": 41943040,
      "expires_at": "2024-01-31T00:00:00Z",
      "created_at": "2024-01-01T00:00:00Z"
    }
  ],
  "restore_window": {
    "earliest_restore_at": "2023-12-25T00:00:00Z",
    "latest_restore_at": "2024-01-01T00:00:00Z"
  }
}

Newest first. source is manual or scheduled; only manual backups count against the plan's allowance. size_bytes is absent until the storage provider has costed the backup. restore_window is the span a point-in-time restore may target, and is absent on a plan without point-in-time restore. This route keeps working while a restore is running.

A database with no storage behind it yet — typically one still provisioning — answers 409 on the backup and schedule reads. Every other status reads fine, including a database being restored.

Create Backup

POST /projects/{projectId}/databases/{databaseName}/backups
Authorization: Bearer <platform_token>
Content-Type: application/json

Request:

{
  "name": "before_migration"
}

Response: 201 Created with the backup. Backups are taken synchronously — there is nothing to poll.

Names are lowercase letters, numbers, underscores, and hyphens, up to 63 characters, unique within the database. Names beginning with volcano- are reserved for the platform's own snapshots.

StatusMeaning
400Invalid backup name
403Backup allowance reached, or backups are SUPERAGENT-only
404Project or database not found
409Name already exists, a backup was taken less than a minute ago, or the database is restoring
503Backups are temporarily unavailable

Limit: 50 backups per database on SUPERAGENT, counting only backups you took. HOBBY has none — every endpoint in this section answers 403.

Get Backup

GET /projects/{projectId}/databases/{databaseName}/backups/{backupName}
Authorization: Bearer <platform_token>

Returns one backup. A name that does not exist answers 404.

Delete Backup

DELETE /projects/{projectId}/databases/{databaseName}/backups/{backupName}
Authorization: Bearer <platform_token>

Response:

{
  "status": "deleted",
  "message": "backup deleted"
}

Frees the backup's storage. Scheduled backups can be deleted too. A backup that is already gone answers 404, and a database being restored answers 409: the restore may still need the backup it is pinned to.

Get Backup Schedule

GET /projects/{projectId}/databases/{databaseName}/backup-schedule
Authorization: Bearer <platform_token>

Response:

{
  "entries": [
    { "frequency": "daily", "hour": 3, "retention_seconds": 2592000 }
  ]
}

An empty entries list means no automated backups.

Replace Backup Schedule

PUT /projects/{projectId}/databases/{databaseName}/backup-schedule
Authorization: Bearer <platform_token>
Content-Type: application/json

Request:

{
  "entries": [
    { "frequency": "daily", "hour": 3 },
    { "frequency": "weekly", "hour": 4, "day": 6, "retention_seconds": 1209600 }
  ]
}

Replaces the schedule wholesale; an empty list stops automated backups. The response is the stored schedule, including the retention that was applied.

frequency is daily, weekly, or monthly. hour is UTC (0-23). day is the day of the week (1-7, Monday to Sunday) for a weekly entry or the day of the month (1-28) for a monthly one; it is required for both and ignored for a daily entry. retention_seconds defaults to the plan's retention and is clamped to it.

Scheduled backups do not count against the plan's backup allowance.

StatusMeaning
400Invalid frequency, hour, or day
403Backups are SUPERAGENT-only
409The database is not active

List Restores

GET /projects/{projectId}/databases/{databaseName}/restores
Authorization: Bearer <platform_token>

Returns the database's restore history, newest first, capped at the 50 most recent with no pagination. Readable during a restore.

Restore Database

POST /projects/{projectId}/databases/{databaseName}/restores
Authorization: Bearer <platform_token>
Content-Type: application/json

Request: exactly one of backup_name or restore_to.

{
  "backup_name": "before_migration"
}
{
  "restore_to": "2024-01-01T09:30:00Z"
}

Response: 202 Accepted

{
  "id": "7f3a1c2e-5d4b-4a91-8c6f-2b1e9d0a4c73",
  "database_id": "abc-123-456-789",
  "project_id": "11111111-1111-1111-1111-111111111111",
  "kind": "snapshot",
  "status": "pending",
  "backup_name": "before_migration",
  "created_at": "2024-01-01T12:00:00Z",
  "updated_at": "2024-01-01T12:00:00Z"
}

The restore runs in the background with the database in restoring and not accepting connections. Poll Get Restore until it reports completed. Restores are in place and destructive: data written after the restored point is discarded, and the connection string is unchanged throughout.

restore_to must fall inside the restore_window from List Backups.

StatusMeaning
400Both targets or neither, or a time outside the window
403Backups and point-in-time restore are SUPERAGENT-only
404Project, database, or backup not found
409A restore is already in progress, the database is not active, another database operation is running, or too many pre-restore states are still held open
503Restores are temporarily unavailable

Get Restore

GET /projects/{projectId}/databases/{databaseName}/restores/{restoreId}
Authorization: Bearer <platform_token>

Response:

{
  "id": "7f3a1c2e-5d4b-4a91-8c6f-2b1e9d0a4c73",
  "database_id": "abc-123-456-789",
  "project_id": "11111111-1111-1111-1111-111111111111",
  "kind": "point_in_time",
  "status": "completed",
  "restore_to": "2024-01-01T09:30:00Z",
  "completed_at": "2024-01-01T12:03:20Z",
  "created_at": "2024-01-01T12:00:00Z",
  "updated_at": "2024-01-01T12:03:20Z"
}

kind is snapshot or point_in_time, and the restore carries whichever of backup_name and restore_to it was started with. status is pending, running, completed, failed, or exhausted; an attempt that fails with tries left goes back to pending, while failed and exhausted both mean Volcano gave up. Either leaves the database failed if its data may already have been replaced, and active if the restore never started — a backup that is gone from the provider ends the restore without touching the database. error carries why the most recent attempt failed.

Database Query API

Query databases directly via REST API - no SQL required!

SELECT - Query Data

POST /databases/{databaseName}/query/select
Authorization: Bearer <auth_user_access_token>
Content-Type: application/json

Request:

{
  "table": "posts",
  "select": ["id", "title", "content"],
  "filters": [
    { "column": "status", "operator": "eq", "value": "published" },
    { "column": "views", "operator": "gt", "value": 100 }
  ],
  "order": [
    { "column": "created_at", "ascending": false }
  ],
  "limit": 10,
  "offset": 0
}

Response:

{
  "data": [
    {
      "id": "uuid",
      "title": "My Post",
      "content": "Post content",
      "status": "published",
      "views": 150,
      "created_at": "2026-01-13T10:00:00Z"
    }
  ],
  "count": 1
}

Authentication: Requires auth user access token (from signup/signin), not platform token

Row-Level Security: Automatically enforced - users see only their own data

Filter Operators:

  • eq (equals)
  • neq (not equals)
  • gt (greater than)
  • gte (greater than or equal)
  • lt (less than)
  • lte (less than or equal)
  • like (pattern match, case-sensitive)
  • ilike (pattern match, case-insensitive)
  • is (NULL check)
  • in (array membership)

See Also: REST API Guide | Query Builder API

INSERT - Create Data

POST /databases/{databaseName}/query/insert
Authorization: Bearer <auth_user_access_token>
Content-Type: application/json

Request:

{
  "table": "posts",
  "values": {
    "title": "My New Post",
    "content": "Content here",
    "status": "draft"
  }
}

Response:

{
  "data": [
    {
      "id": "uuid",
      "title": "My New Post",
      "content": "Content here",
      "status": "draft",
      "user_id": "user-uuid",
      "created_at": "2026-01-13T10:00:00Z"
    }
  ],
  "count": 1
}

Note: user_id is automatically set to the authenticated user via trigger using auth.uid()

UPDATE - Modify Data

POST /databases/{databaseName}/query/update
Authorization: Bearer <auth_user_access_token>
Content-Type: application/json

Request:

{
  "table": "posts",
  "values": {
    "title": "Updated Title",
    "status": "published"
  },
  "filters": [
    { "column": "id", "operator": "eq", "value": "post-uuid" }
  ]
}

Response:

{
  "data": [
    {
      "id": "post-uuid",
      "title": "Updated Title",
      "status": "published",
      "updated_at": "2026-01-13T10:05:00Z"
    }
  ],
  "count": 1
}

Row-Level Security: If RLS blocks the update (not your data), returns empty array

DELETE - Remove Data

POST /databases/{databaseName}/query/delete
Authorization: Bearer <auth_user_access_token>
Content-Type: application/json

Request:

{
  "table": "posts",
  "filters": [
    { "column": "id", "operator": "eq", "value": "post-uuid" }
  ]
}

Response:

{
  "data": [
    {
      "id": "post-uuid",
      "title": "Deleted Post"
    }
  ],
  "count": 1
}

Safety: UPDATE and DELETE require at least one filter to prevent accidental mass changes

Row-Level Security: If RLS blocks the delete (not your data), returns empty array


Get Database Stats

GET /projects/{projectId}/databases/{databaseName}/stats
Authorization: Bearer <platform_token>

Query Parameters:

  • from - Start time (RFC3339, default: 24h ago)
  • to - End time (RFC3339, default: now)
  • granularity - hourly, daily, monthly (default: hourly)

Response:

{
  "current_storage_bytes": 1610612736,
  "current_storage_mb": 1536.0,
  "branches": [
    { "id": "9b1f0f4c-2b8e-4f43-9a71-1f6c2f0f2c31", "name": "feature_checkout", "storage_bytes": 536870912 }
  ],
  "backup_storage_bytes": 429496729,
  "storage_bytes": 12345678,
  "data_written_bytes": 9876543,
  "data_transfer_bytes": 5432109,
  "compute_time_seconds": 123.45,
  "active_time_seconds": 456.78,
  "time_range": "2024-01-01T00:00:00Z to 2024-01-02T00:00:00Z",
  "granularity": "hourly"
}

current_storage_bytes is the on-disk size right now: the database itself, plus every branch's divergence from it, plus what its backups cost to hold. This is the figure your storage allowance is enforced against. branches and backup_storage_bytes break it down, and branches is ordered most expensive first.

A branch is charged only for what it has written since the fork. A backup is not: one you take is charged as a full copy of the database as it was then, and a schedule's later snapshots only for what they add. backup_storage_bytes is sampled from the provider rather than measured live, so it can lag a change by a few minutes; it is zero on a plan without backups. See what backups cost.

storage_bytes is the historical synthetic storage figure from the consumption API and is unrelated.


Get Database Queries

GET /projects/{projectId}/databases/{databaseName}/queries
Authorization: Bearer <platform_token>

Returns the database's current top queries from pg_stat_statements, ranked by total execution time.

SUPERAGENT plan required.

Query Parameters:

  • limit - Maximum number of queries to return, from 1 to 100 (default: 10)

Response:

{
  "data": [
    {
      "query_id": "123456789",
      "query": "select * from posts where user_id = ?",
      "database": {
        "id": "11111111-1111-1111-1111-111111111111",
        "name": "app"
      },
      "role": "authenticated",
      "calls": 42,
      "total_exec_time_seconds": 1.25,
      "max_exec_time_seconds": 0.2,
      "mean_exec_time_seconds": 0.03,
      "min_exec_time_seconds": 0.01,
      "rows_processed": 420
    }
  ]
}

Database Status

  • provisioning - Being created (5-10 seconds)
  • active - Ready to use
  • restoring - Being restored; not connectable, and most operations on it return 409
  • failed - Creation failed, or a restore gave up after its data may have been replaced
  • deleting - Being torn down

See Also

On this page