Branching
By the end of this page you will be able to fork a ScramDB database instantly, connect to the fork like any other database, and use that fork for testing, debugging, CI, and agent sandboxes.
Branching is ScramDB's "give me a working copy of the database right now" feature. It runs entirely in SQL, against a live, running server, and it is instant because it does not copy data: a branch is a copy-on-write fork that shares storage with its parent at the moment it is created.
The two formsβ
CLONE: fork the current stateβ
CREATE DATABASE staging_test CLONE scramdb;
Instant, present-state, copy-on-write fork of scramdb into a new database called staging_test. It needs zero extra configuration, it works out of the box on any single-node instance.
PostgreSQL-familiar spellings work identically, they are accepted synonyms rewritten to the same operation:
CREATE DATABASE staging_test2 TEMPLATE scramdb;
CREATE DATABASE staging_test3 TEMPLATE = scramdb; -- the = form is equivalent
FROM ... AT TIMESTAMP: fork a past stateβ
CREATE DATABASE audit_before_incident FROM scramdb AT TIMESTAMP '2026-08-02T09:30:00Z';
Forks scramdb as it existed at the given timestamp, rather than as it exists right now. This requires [storage.wal.backup].destination to be configured and a base backup to already exist there, plus WAL history back to your target timestamp, either in the local WAL directory or the WAL archive. See Backups to set that up if you have not already; if the config key is missing, the statement fails loud naming it directly, it does not fail silently or produce an empty database.
The timestamp must be an RFC3339 literal, e.g. '2026-08-02T09:30:00Z' or '2026-08-02T09:30:00.123456Z' for microsecond precision.
There is also a function-call form, equivalent to the SQL syntax above:
SELECT scram.branch_at('scramdb', 'audit_before_incident', '2026-08-02T09:30:00Z');
A bare CREATE DATABASE child FROM parent; with no AT TIMESTAMP clause is not valid syntax, it is a hard parse error. FROM is only ever followed by AT TIMESTAMP '<literal>'.
What copy-on-write means hereβ
At the moment of the fork, the child database shares its parent's underlying storage: nothing is physically copied, which is why the fork completes instantly regardless of how large the parent database is. From that point on, the two databases are fully independent. A write to the parent after the fork never appears in the child, and a write to the child never appears in the parent; each side only consumes new storage for the data it actually changes. You get an isolated, full-sized working copy at the storage cost of the changes you make to it, not the size of the whole database.
Connecting to a branchβ
A branch is a real, independently-connectable database, reachable by name over the normal PostgreSQL wire protocol, exactly like any other database on the instance. It is not a special mode of the parent's connection, and it does not require a restart, tables created by the branch are visible immediately.
psql "host=127.0.0.1 port=5432 user=scramdb dbname=staging_test"
Any PostgreSQL-compatible driver works the same way, point its dbname (or equivalent connection parameter) at the branch's name.
Cleaning up a branchβ
DROP DATABASE staging_test;
This works even if the branch has tables and data in it, DROP DATABASE tears down every table in the branch first, releasing storage the branch changed while leaving data still shared with the parent untouched. DROP DATABASE IF EXISTS staging_test; is also available if you are not sure the branch still exists.
The default database of an instance can never be dropped; ScramDB refuses that outright.
Worked workflowsβ
1. Testing against real dataβ
Clone production, try something risky against the clone, throw it away.
CREATE DATABASE migration_test CLONE scramdb;
psql "host=127.0.0.1 port=5432 user=scramdb dbname=migration_test" \
-c "ALTER TABLE orders ADD COLUMN priority integer DEFAULT 0;" \
-c "UPDATE orders SET priority = 1 WHERE status = 'urgent';"
Verify the migration did what you expected against real data volumes and real data shapes:
psql "host=127.0.0.1 port=5432 user=scramdb dbname=migration_test" \
-c "SELECT priority, count(*) FROM orders GROUP BY priority;"
DROP DATABASE migration_test;
Production was never touched, and you tested against the actual data, not a synthetic fixture.
2. Debugging production dataβ
Branch as of just before an incident, then compare it against the current state to see exactly what changed.
CREATE DATABASE audit_before_incident FROM scramdb AT TIMESTAMP '2026-08-02T09:30:00Z';
psql "host=127.0.0.1 port=5432 user=scramdb dbname=audit_before_incident" \
-c "SELECT id, status FROM orders WHERE id = 101;"
psql "host=127.0.0.1 port=5432 user=scramdb dbname=scramdb" \
-c "SELECT id, status FROM orders WHERE id = 101;"
Comparing the two outputs shows you exactly what the row looked like right before the incident versus now, without touching production and without a separate restore-to-a-new-server round trip.
DROP DATABASE audit_before_incident;
3. CIβ
Give every test run its own isolated copy of a known-good fixture database, with no per-run data setup cost.
RUN_ID=$(date +%s)
psql "host=127.0.0.1 port=5432 user=scramdb dbname=scramdb" \
-c "CREATE DATABASE ci_run_${RUN_ID} CLONE template_db;"
# run your test suite against dbname=ci_run_${RUN_ID}
psql "host=127.0.0.1 port=5432 user=scramdb dbname=scramdb" \
-c "DROP DATABASE ci_run_${RUN_ID};"
Every run starts from the exact same fixture state, instantly, and cleans up after itself. No shared mutable test database, no fixture-loading step slowing down the pipeline.
4. Agent sandboxesβ
Give each autonomous job or agent its own branch to read and write freely, fully isolated from production and from every other agent's branch.
CREATE DATABASE agent_task_4471 CLONE scramdb;
The agent connects to agent_task_4471 and can run arbitrary reads and writes, including ones you would never let it run against production, since nothing it does there can reach the parent database or any other agent's branch.
DROP DATABASE agent_task_4471;
Known constraintsβ
- Single-node only, today. Both
CLONEandFROM ... AT TIMESTAMPare refused outright on a clustered deployment, with a named error explaining that branching needs a base backup, timestamp, and database identity resolved consistently across nodes, a capability the cluster does not yet have. Run branching against a single-node instance. - Not inside an explicit transaction block.
CREATE DATABASE(either form) is refused if you run it insideBEGIN ... COMMIT. Run it as its own statement. - The default database can never be cloned away or dropped. Every instance's default database is protected from
DROP DATABASE, regardless of how many branches exist. FROM ... AT TIMESTAMPneeds its prerequisites in place. See is your instance configured for this? for the checklist: a configured backup destination, a base backup taken to it, and WAL history reaching back to your target timestamp.
Nextβ
- Backups: set up the backup location that
FROM ... AT TIMESTAMPdepends on. - Point-in-time recovery: the same backup-plus-WAL machinery, used to rebuild a whole fresh instance instead of forking a database.