Branching
Fork a database into a short-lived, isolated copy for development, testing, and CI.
A branch is a copy-on-write fork of one of your databases. It starts as an exact copy of the parent's data — schema, rows, roles, and row-level security policies — and diverges from there. Writing to a branch never touches the parent.
Branches are for work you would not want to do against production data: trying a migration, running an integration suite, reproducing a bug against real-shaped data, giving a pull request its own database.
Create one, wait for it to become active, then connect to it exactly as you
connect to any other Volcano database:
# Create the branch
curl -X POST https://api.volcano.dev/projects/$PROJECT_ID/databases/main_db/branches \
-H "Authorization: Bearer $VOLCANO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "feature_checkout", "ttl_seconds": 86400}'{
"id": "9b1f0f4c-2b8e-4f43-9a71-1f6c2f0f2c31",
"database_id": "abc-123-456-789",
"project_id": "11111111-1111-1111-1111-111111111111",
"name": "feature_checkout",
"status": "provisioning",
"ttl_seconds": 86400,
"expires_at": "2024-01-02T00:00:00Z",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}Creation is asynchronous. The response is 202 with the branch provisioning
and no connection string. Poll until it reports active:
curl https://api.volcano.dev/projects/$PROJECT_ID/databases/main_db/branches/feature_checkout \
-H "Authorization: Bearer $VOLCANO_TOKEN"{
"id": "9b1f0f4c-2b8e-4f43-9a71-1f6c2f0f2c31",
"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": 0,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:05Z"
}Forks usually take under a minute. Volcano retries a fork that does not take, so
keep waiting while the branch reports provisioning. A branch reports failed
only once Volcano has stopped retrying; delete it and try again.
Every branch expires
expires_at is a hard deadline, not a hint. When it passes, the branch stops
accepting connections and is deleted along with its data. This is deliberate: a
fork you forgot about still costs storage and still holds a copy of your
production data.
ttl_seconds sets the lifetime, between one hour and 30 days. It defaults to
seven days.
Extend a branch you are still working on. The countdown restarts from now:
curl -X PATCH https://api.volcano.dev/projects/$PROJECT_ID/databases/main_db/branches/feature_checkout \
-H "Authorization: Bearer $VOLCANO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ttl_seconds": 604800}'The new duration is remembered, so a later reset re-arms the same lifetime.
Connecting to a branch
A branch has its own connection string, its own username, and its own password.
Everything you know about connecting to a Volcano database applies unchanged,
including how application_name selects the access mode:
application_name | Access |
|---|---|
volcano_full_access | Full admin access — DDL, migrations |
volcano_user_access:{user_id} | Acts as that end user; RLS enforced |
volcano_user_access | Anonymous (anon role); RLS enforced |
psql "$BRANCH_CONNECTION_STRING"The REST query API works the same way, under a branch-scoped path:
curl -X POST https://api.volcano.dev/databases/main_db/branches/feature_checkout/query/select \
-H "Authorization: Bearer $SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{"table": "posts", "select": ["id", "title"], "limit": 10}'Service keys and end-user tokens are project-scoped, so the same tokens that reach the parent reach its branches. Anonymous keys are not accepted on the query API, on a branch or on a parent.
A branch that exists but cannot serve queries yet — still provisioning, being
reset, expired, or with its parent being restored — answers 503, not 404, so
polling can tell it apart from a name that was never there.
A branch's password only works for that branch. Handing a branch connection string to a test runner or a preview environment does not hand it access to your production data.
Deployed functions and frontends always see the parent. DATABASE_URL and the
project variables Volcano manages point at the primary database and are never
rewritten to a branch. To target a branch from your own code, pass its
connection string explicitly.
Resetting a branch
Reset discards everything written to the branch and re-forks it from the parent as it is right now:
curl -X POST https://api.volcano.dev/projects/$PROJECT_ID/databases/main_db/branches/feature_checkout/reset \
-H "Authorization: Bearer $VOLCANO_TOKEN"The call returns 202 immediately with the branch back in provisioning. Poll
it until it reports active, the same way you do after creating one:
curl https://api.volcano.dev/projects/$PROJECT_ID/databases/main_db/branches/feature_checkout \
-H "Authorization: Bearer $VOLCANO_TOKEN"The branch keeps its name and its connection string, so anything holding that string keeps working once it is active again, and its lifetime is re-armed. The branch does not serve connections while the reset runs.
This is what makes a branch reusable in CI: reset before each run instead of creating and destroying a branch every time.
One thing delays a reset: for up to 24 hours after the parent database is
restored, reset returns 409. The branch
keeps serving its own data throughout — only the rewind is refused — and a reset
after that lands on the restored parent data.
Rotating a branch's password
curl -X POST https://api.volcano.dev/projects/$PROJECT_ID/databases/main_db/branches/feature_checkout/reset-password \
-H "Authorization: Bearer $VOLCANO_TOKEN"The response carries a new connection string; the previous one stops authenticating new connections within a few seconds. Open connections are not interrupted. Rotation stops at the branch you name: the parent database's credentials and every sibling branch's are untouched.
Deleting a branch
curl -X DELETE https://api.volcano.dev/projects/$PROJECT_ID/databases/main_db/branches/feature_checkout \
-H "Authorization: Bearer $VOLCANO_TOKEN"The branch stops accepting connections immediately and is removed in the
background. 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.
What a branch costs
A branch shares pages with its parent, so it is not charged for the data it copied. What counts is how far it has diverged — the bytes it has written since the fork:
database storage = parent size
+ Σ (each branch's divergence)
+ Σ (each backup's cost)That total is what your plan's storage allowance is enforced against. A
backup is not charged on the same terms: a branch
shares its parent's pages, while a backup you take is charged as a full copy of
the database. storage_bytes on a branch is its divergence, and the database
stats endpoint breaks the total down per branch, most expensive first:
curl https://api.volcano.dev/projects/$PROJECT_ID/databases/main_db/stats \
-H "Authorization: Bearer $VOLCANO_TOKEN"{
"current_storage_bytes": 1610612736,
"current_storage_mb": 1536.0,
"branches": [
{ "id": "9b1f0f4c-2b8e-4f43-9a71-1f6c2f0f2c31", "name": "feature_checkout", "storage_bytes": 536870912 },
{ "id": "3c2d1e0f-9a8b-7c6d-5e4f-3a2b1c0d9e8f", "name": "nightly_ci", "storage_bytes": 0 }
],
"backup_storage_bytes": 0
}A freshly forked branch, or one that has only been read from, contributes nothing. Resetting a branch releases what it had diverged by.
Requests against a branch count toward your database request allowance the same as requests against the parent, and appear in the parent's query logs.
Limits
| HOBBY | SUPERAGENT | |
|---|---|---|
| Branches per database | 10 | 25 |
The allowance is per database, not per project, and counts branches in every
state — a branch that is still provisioning holds a slot. Creating a branch over
the allowance returns 403.
Naming
Branch names are lowercase letters, numbers, and underscores, up to 64
characters, and unique within their parent database. Two different databases can
each have a branch called staging.
Creating a branch with a name that already exists returns 409 rather than a
second branch, so retrying a create that timed out cannot silently consume two
slots.
Statuses
provisioning— being forked, rebuilt after a reset, or waiting on a retry; not connectable, no connection string. Keep polling.active— ready to usefailed— terminal. Volcano stopped retrying and the branch will not becomeactiveon its own; delete it and retrydeleting— torn down; no longer accepts connections
See Also
- Databases API — full endpoint reference
- Connection Strings
- Row-Level Security