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.
From the CLI
Every operation on this page is also a volcano cloud databases branches
command:
| Task | Command |
|---|---|
| Create a branch | volcano cloud databases branches create main_db feature_checkout --ttl 24h |
| Show one, with its connection string | volcano cloud databases branches get main_db feature_checkout --show-connection-string |
| List a database's branches | volcano cloud databases branches list main_db |
| Extend its lifetime | volcano cloud databases branches extend main_db feature_checkout --ttl 168h |
| Reset it to the parent's data | volcano cloud databases branches reset main_db feature_checkout |
| Rotate its password | volcano cloud databases branches rotate-password main_db feature_checkout |
| Delete it | volcano cloud databases branches delete main_db feature_checkout |
--ttl takes a duration between 1h and 720h, and defaults to 168h.
create returns while the branch is still provisioning; run get until it
reports active to see the connection string. reset, rotate-password, and
delete ask for confirmation unless you pass --yes.
Branching is a cloud feature. Databases in local mode have no branches.
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.
A branch per pull request
Give every pull request its own copy of the data: fork a branch on the first
push, reset it on every later push, apply the pull request's new migrations and
run the tests against it, and delete it when the pull request closes. This
script is the per-push step; it needs curl, jq, psql, and a checkout that
has fetched the base branch:
set -euo pipefail
API="https://api.volcano.dev/projects/$PROJECT_ID/databases/main_db/branches"
AUTH="Authorization: Bearer $VOLCANO_TOKEN"
BRANCH="pr_${PR_NUMBER}"
BASE="origin/${BASE_REF:-main}"
# The first push forks the branch. A later push finds it already there (409)
# and rewinds it to the parent's current data instead.
code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST "$API" \
-H "$AUTH" -H "Content-Type: application/json" \
-d "{\"name\": \"$BRANCH\", \"ttl_seconds\": 259200}")
case "$code" in
202) ;;
409) curl -fsS -o /dev/null -X POST "$API/$BRANCH/reset" -H "$AUTH" ;;
*) echo "creating $BRANCH returned $code" >&2; exit 1 ;;
esac
# Both calls return while the branch is provisioning. Wait for it.
while :; do
branch=$(curl -fsS "$API/$BRANCH" -H "$AUTH")
case "$(jq -r .status <<<"$branch")" in
active) break ;;
failed) echo "$BRANCH failed to fork" >&2; exit 1 ;;
esac
sleep 5
done
BRANCH_URL=$(jq -r .connection_string <<<"$branch")
# The branch holds the parent's schema, so apply only the migrations this
# pull request adds, in order, then test against the result.
git diff --name-only --diff-filter=A "$BASE"...HEAD -- 'volcano/migrations/*.sql' \
| sort \
| while read -r f; do psql "$BRANCH_URL" -v ON_ERROR_STOP=1 -f "$f"; done
DATABASE_URL="$BRANCH_URL" npm testRun the cleanup from the job that fires when the pull request closes:
curl -fsS -X DELETE "$API/$BRANCH" -H "$AUTH"Nothing is lost if that job never runs. The branch expires 72 hours after its last fork or reset, and each reset keeps its connection string, so a long-lived pull request keeps one branch rather than piling up new ones. Because every push starts from a fresh copy of the parent, the pull request's migrations run against the schema they will meet in production, never against the leftovers of the previous run.
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.
If Volcano cannot read your subscription pricing, branch creation returns 503
with "subscription pricing temporarily unavailable". Retry later; no branch is
created.
A migration run's rehearsal branch
counts toward the allowance and costs like any branch while it exists. A run
that would take the database over its allowance fails with branch_limit
before it changes anything.
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.
Names beginning with migration_rehearsal_ are reserved, and creating one returns
400. A migration run rehearses its files on a short-lived branch with such a
name. It is listed with your branches and holds a slot of the allowance until the
run deletes it.
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