Branches_
Spin up an ephemeral, isolated copy of your PostgreSQL database in seconds from a storage snapshot. Use branches for previews, migrations, and testing.
3 min read
A branch is a short-lived, isolated copy of your database. It has its own endpoint and reuses the parent's credentials, because it is a snapshot copy of the parent's storage volume taken at a point in time. Branches are not replicas: once created, they diverge from the parent and never sync back.
There is no branch merge operation. Use a branch to validate a migration, data repair, or application change, then intentionally cut application traffic over to the validated database or copy the data you want back with engine-native tools. Appwrite does not reconcile two diverged database histories for you.
Use cases:
- Preview environments: one branch per pull request, destroyed when the PR closes
- Test migrations: apply a destructive
ALTERagainst the branch first, observe the behavior, then run it against the source - Reproduce a bug: branch the database, attach a debugger, throw the branch away when done
- Heavy analytical queries:
EXPLAIN ANALYZEexperiments against a branch cannot slow down the primary
How it works
Creating a branch snapshots the parent's storage volume and provisions a branch instance from that snapshot, on the same engine version, with its own isolated storage. PostgreSQL snapshots are crash-consistent, so the parent is never frozen and takes no write pause. Branch compute is fixed and lightweight, enough to validate a change rather than carry production load, and is not configurable. Provisioning is asynchronous and usually takes a few minutes, because the storage provider finalizes the snapshot before the branch instance starts.
Create a branch
Both fields are optional:
| Field | Default | Purpose |
|---|---|---|
branchId | auto-generated | Custom ID (a-z, A-Z, 0-9, ., -, _, max 36 chars) |
ttl | 86400 (24 hours) | Lifetime in seconds before the branch expires (min 300, max 604800) |
The call is asynchronous and returns immediately while the branch provisions in the background. Every branch has a TTL. Leaving ttl out gives you 24 hours, and the longest branch you can create is 7 days, so a branch is never a permanent resource.
The value you send as branchId is stored as the branch name. The list response returns a separate generated branchId, so look up a branch by its name rather than by the value you passed.
List branches and connect
Each entry carries its metadata and connection details, so there is no separate credentials call:
A branch gets its own hostname, and reuses the parent's username and password because it is a snapshot copy of the parent's storage. The port is the standard 5432; branches have no connection pooler. Connect with the branch's connectionString straight from the response:
psql "<branch connectionString>"Delete a branch
Deleting a branch removes the branch's instance, its storage volume, and the underlying snapshot. There is no soft delete: once the branch is gone, the data is gone.
Billing
A branch is billed for the parent's full provisioned storage from the moment it is created, pro-rated for how long the branch exists. Divergence is not measured, so a branch you never write to costs the same as one you rewrite completely. Short-lived branches are therefore much cheaper than long-lived ones. There is no separate branch line item, branches roll into your regular database storage totals.
Use case: a development copy of production
Branches also separate daily development from production without maintaining seed scripts. Create a branch from the production database and point local and staging environments at the branch's hostname. Developers query production-shaped data, and every write stays on the branch, so production is never at risk from a bad migration or a careless DELETE.
Because a branch expires after 7 days at most, treat this as a recurring refresh rather than a standing environment. Delete the branch and create a new one with the same name on a schedule that suits your team. Each new branch starts from the parent's current state, which also keeps the development data from drifting far from production.
Use case: per-PR preview database
A CI pipeline that branches on every pull request and tears down on close:
name: preview-database
on: pull_request: types: [opened, reopened, closed]
jobs: branch: if: github.event.action != 'closed' runs-on: ubuntu-latest steps: - name: Create branch run: | curl -X POST \ -H "X-Appwrite-Project: ${{ vars.APPWRITE_PROJECT_ID }}" \ -H "X-Appwrite-Key: ${{ secrets.APPWRITE_API_KEY }}" \ -H "Content-Type: application/json" \ -d '{"branchId": "pr-${{ github.event.number }}", "ttl": 604800}' \ https://<REGION>.cloud.appwrite.io/v1/postgresql/${{ vars.DATABASE_ID }}/branches
teardown: if: github.event.action == 'closed' runs-on: ubuntu-latest steps: - name: Delete branch run: | curl -X DELETE \ -H "X-Appwrite-Project: ${{ vars.APPWRITE_PROJECT_ID }}" \ -H "X-Appwrite-Key: ${{ secrets.APPWRITE_API_KEY }}" \ https://<REGION>.cloud.appwrite.io/v1/postgresql/${{ vars.DATABASE_ID }}/branches/pr-${{ github.event.number }}Branch creation does not deduplicate by name. Calling it twice with the same branchId provisions a second branch, so reopening a pull request leaves you with two. Delete the existing branch before recreating it, or list branches first and skip creation when one already matches.
Was this page helpful?
Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.