Branches
Database branches are independent copy-on-write clones of a parent PostgreSQL database. Each branch has its own connection credentials, network rules, metrics, and configurable lifespan — enabling safe schema experimentation, preview environments, and team isolation.
Overview
Every project starts with a main branch that is default, protected, and permanent. You create feature or preview branches off main (or any other active branch) to safely test schema changes, seed different datasets, or give each team member an isolated environment.
main (default, protected, permanent)
├── feature-auth (lifespan: 7d, expires_at: 2025-07-09)
├── staging (lifespan: forever)
└── preview-pr-42 (lifespan: 7d, expires_at: 2025-07-10)Each branch is a fully independent PostgreSQL database. Writes to one branch never affect another.
Branch limits
| Limit | Value |
|---|---|
| Maximum active branches per project | 10 |
| Minimum branch name length | 1 character |
| Allowed characters | Lowercase letters, digits, hyphens (-), underscores (_) |
| Reserved name | main (cannot be created manually) |
Exceeding 10 active branches returns 429 BranchLimitExceeded. Delete an expired or unused branch first.
Lifespans
When creating a branch, choose a lifespan. Once expired, the branch is marked expired and credentials stop working.
| Value | Accepted aliases | Duration |
|---|---|---|
7d | 7days, 7 days | 7 days |
1m | 1month, 1 month | 30 days |
6m | 6months, 6 months | 183 days |
1y | 1year, 1 year | 365 days |
forever | permanent | No expiry |
Default when omitted: 7d.
Creating a branch
POST /api/v1/projects/:project_id/branches
Authorization: Bearer <paseto-v4-token>
Content-Type: application/json
{
"branch_name": "feature-auth",
"source_branch_id": null,
"lifespan": "7d"
}source_branch_id— UUID of the branch to clone from. Defaults to the project'smainbranch if omitted.lifespan— One of the accepted lifespan values above.
Response (201 Created):
{
"id": "...",
"project_id": "...",
"branch_name": "feature-auth",
"database_name": "sq_servers_prod_br_feature-auth",
"source_database": "sq_servers_prod",
"parent_branch_id": "...",
"status": "active",
"lifespan": "7d",
"expires_at": "2025-07-09T00:00:00Z",
"ttl_seconds": 604800,
"is_default": false,
"protected": false,
"created_by": "...",
"tenant_id": "...",
"created_at": "2025-07-02T00:00:00Z"
}When a branch is created, the gateway automatically whitelists the creator's IP on the new branch (as a branch-scoped network rule). This means you can connect immediately without a manual axm network allow step.
What happens during branch creation?
- The gateway checks the project's storage quota via
square-dbctl project-usage. square-dbctl branch-createis called with--source {source_database} --target {branch_database}.- PostgreSQL creates a database copy using template-based instantiation.
- The branch record is inserted into
project_branches. - An
audit_eventsrow is written:branch.created. - The creator's IP is automatically added as a
branch-scoped CIDR allowlist rule.
Listing branches
GET /api/v1/projects/:project_id/branches
Authorization: Bearer <paseto-v4-token>Returns all non-deleted branches ordered by is_default DESC, created_at ASC (so main always appears first).
Looking up a branch by name
GET /api/v1/projects/:project_id/branches/lookup?name=feature-auth
GET /api/v1/projects/:project_id/branches/lookup?id=<uuid>Returns { "matches": [...], "is_duplicate": false }.
Branch credentials
Get Prisma-ready connection strings for any branch:
GET /api/v1/projects/:project_id/branches/:branch_ref/credentials
Authorization: Bearer <paseto-v4-token>branch_ref can be a branch UUID or a branch name (e.g. feature-auth).
Response:
{
"project_id": "...",
"branch_id": "...",
"branch_name": "feature-auth",
"database": "sq_servers_prod_br_feature-auth",
"runtime_key": "DATABASE_URL_SERVERS_PROD_BR_FEATURE_AUTH",
"direct_key": "DIRECT_URL_SERVERS_PROD_BR_FEATURE_AUTH",
"database_url": "postgresql://servers_prod_rw:...@db.squareexp.com:6432/sq_servers_prod_br_feature-auth?sslmode=require",
"direct_url": "postgresql://servers_prod_owner:...@db.squareexp.com:5432/sq_servers_prod_br_feature-auth?sslmode=require"
}Fetching credentials always writes a branch.credentials.viewed entry to audit_events. This is intentional — credential access is a security-sensitive operation.
Using branch credentials with Prisma
# .env (feature branch)
DATABASE_URL="postgresql://servers_prod_rw:...@db.squareexp.com:6432/sq_servers_prod_br_feature-auth?sslmode=require"
DIRECT_URL="postgresql://servers_prod_owner:...@db.squareexp.com:5432/sq_servers_prod_br_feature-auth?sslmode=require"npx prisma migrate dev # runs against the branch — safe, isolated
npx prisma db push # experimental / fast schema syncBranch metrics
GET /api/v1/projects/:project_id/branches/:branch_ref/metrics/summary
Authorization: Bearer <paseto-v4-token>Returns live database metrics (size, active connections, cache hit ratio) plus project-level usage (storage used / limit):
{
"project_id": "...",
"branch_id": "...",
"branch_name": "feature-auth",
"database": "sq_servers_prod_br_feature-auth",
"metrics": {
"database_size_bytes": 8388608,
"active_connections": 3,
"cache_hit_ratio": 0.97
},
"usage": {
"storageUsedBytes": 52428800,
"storageLimitBytes": 10737418240
}
}Branch table browser
List all tables in a branch:
GET /api/v1/projects/:project_id/branches/:branch_ref/tables
Authorization: Bearer <paseto-v4-token>Browse rows in a specific table (up to 50 rows, paginated):
GET /api/v1/projects/:project_id/branches/:branch_ref/tables/:table/rows?limit=50&offset=0
Authorization: Bearer <paseto-v4-token>Update a single cell (requires owner, admin, or operator role):
PATCH /api/v1/projects/:project_id/branches/:branch_ref/tables/:table/rows
Authorization: Bearer <paseto-v4-token>
Content-Type: application/json
{
"column": "status",
"id": "123",
"primary_key": "id",
"value": "active"
}Delete rows (up to 50 at once, requires owner, admin, or operator role):
DELETE /api/v1/projects/:project_id/branches/:branch_ref/tables/:table/rows
Authorization: Bearer <paseto-v4-token>
Content-Type: application/json
{
"ids": ["123", "456"],
"primary_key": "id"
}Deleting a branch
DELETE /api/v1/projects/:project_id/branches/:branch_id
Authorization: Bearer <paseto-v4-token>Protected and default branches cannot be deleted. The main branch is always protected. Attempting to delete it returns 403 Forbidden.
Deletion soft-deletes the branch in the control plane (status = 'deleted', deleted_at = now()). The underlying PostgreSQL database is dropped asynchronously. An audit_events row is written: branch.deleted.
Branch statuses
| Status | Description |
|---|---|
active | Live and connectable |
expired | TTL elapsed; credentials no longer work |
deleting | Deletion in progress |
deleted | Soft-deleted; control plane record retained for audit |
failed | Creation failed |
Data model
CREATE TABLE project_branches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
parent_branch_id UUID REFERENCES project_branches(id) ON DELETE SET NULL,
branch_name TEXT NOT NULL,
database_name TEXT NOT NULL UNIQUE,
source_database TEXT NOT NULL, -- parent database snapshot source
status TEXT NOT NULL DEFAULT 'active',
lifespan TEXT NOT NULL DEFAULT '7d',
expires_at TIMESTAMPTZ,
ttl_seconds BIGINT,
is_default BOOLEAN NOT NULL DEFAULT false,
protected BOOLEAN NOT NULL DEFAULT false,
created_by UUID NOT NULL REFERENCES users(id),
tenant_id TEXT NOT NULL,
deleted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(project_id, branch_name)
);How is this guide?
Tables & Schema
Explore your tables, visualize database schemas, browse rows, inspect columns, and perform safe data updates in AxiomDB.
Network & Access Control
AxiomDB uses CIDR-based IP allowlisting to control which IPs can connect to your databases. By default all projects start in "restricted" mode — no external connections are allowed until you add a rule.
