Best dev DB for agents
A test database cloned straight from your production. No seeding. No dumps.
Install
No database to hand? Start on a sample one
Install
No sudo — it builds a Linux machine first.
No database to hand? Start on a sample one
Two core features
1.Data and schema replicated in real time
Not only DML: schema changes on the primary are reflected in real time as well.
2.Database branching
Save a particular state of the database and keep restoring to that state as you test and run QA.
Seeding the data every time I set up a test database, a bug in production that would not reproduce in the test environment, and QA that meant testing the same scenario over and over — it was tiresome enough that I built this.
It replicates the primary’s DML and DDL in real time, and you snapshot a particular state of that copy, so every test runs against the same data.
Use it if
- Production is on RDS, Azure Database for PostgreSQL, Cloud SQL or a machine you own, and you are not moving it.
- You need real production data to test against, and it cannot leave your own infrastructure.
- When your agent needs to test against production data without consequences.
- When applying every schema change to the test database has become a chore.
Don’t use it if
- You cannot enable
wal_level=logicalor create a replication slot on the primary. - You already use Neon as your primary database.
How is this different from other CoW Postgres databases?
| Rows follow production | Schema follows | Snapshots / branching | Your own infrastructure | Your production stays put | |
|---|---|---|---|---|---|
| Neon | ○logical replication | ✕yours to keep in step | ○copy-on-write branches | ✕open source, but scoped to experiments | △branch a copy hosted by Neon |
| Supabase | ✕branches start with no data | ○from your migration files | △their cloud only | ○documented — but without branching | ✕Supabase-hosted projects only |
| Aurora cloning | ✕a clone is fixed when taken | ○same storage as the source | △15, then it is a full copy | ✕AWS Aurora only | ○clones the cluster you already run |
| DBLab | △managed primaries are re-copied on a schedule | △only at the next full refresh | ○ZFS or LVM thin clones | ○your hardware | ○sits beside it |
| Xata | ○logical replication (pgstream) | ○event triggers replay DDL | ○copy-on-write branches | △self-hosted, on Kubernetes | ✕the copy lives on the Xata platform |
| Snaplicator | ○logical replication, managed included | ○event triggers replay DDL here | ○btrfs copy-on-write | ○one machine you own | ○sits beside it |
- Neon and Supabase branching
- Both are excellent, and if you already run on them, use their branching — a branch there is your data. Neon will also follow an RDS or Cloud SQL primary over logical replication rather than hosting it, and then two things change: the data lands in Neon’s cloud, and the schema becomes yours to keep in step — their guide requires that
the tables in the source database you are replicating from must also exist in the destination database, and they must have the same table names and columns.
- DBLab (Postgres.ai)
- The OSS I took the idea from. What differs here is that Snaplicator uses logical replication to keep data and schema in sync with the dev DB in real time.
- If you run on Supabase
- Branches there start with no data, by design. If you want that data, leave Supabase as your primary and put Snaplicator beside it — and there is almost nothing to prepare, because
wal_levelis alreadylogical, so unlike other managed services there is no parameter to change and no reboot.
Checked against: Neon branching · Neon: replicate from RDS · Supabase branching · Aurora cloning · DBLab data sources · Xata · Postgres: logical replication restrictions · Supabase: replicate to another Postgres
Setup
Before you start
- On your primary
wal_levelhas to belogical(on RDS and Aurora,rds.logical_replication = 1— either way a restart), a replication slot has to be free, and the account needs superuser,rds_superuser, orREPLICATION. Superuser also decides whether the schema follows, becauseCREATE EVENT TRIGGERtakes nothing less.- On the machine that will run it
- Linux with docker and python 3.10 or newer, run as root, plus disk for the pool. On macOS the installer builds the Linux machine itself.
All of it is checked before anything is created, and a failure names the setting that is missing.
Install
One line. It then asks for a connection URI to your primary — or type
demoto start on a sample database instead.$ curl -fsSL https://raw.githubusercontent.com/bhpark1013/Snaplicator/main/deploy/install.sh | sudo bash$ curl -fsSL https://raw.githubusercontent.com/bhpark1013/Snaplicator/main/deploy/install.sh | bashbtrfs is a Linux filesystem, so there is nothing on a Mac to install onto. The installer builds a Linux machine with OrbStack and continues inside it — no sudo. If OrbStack is not there it offers to install it; commercial use needs a paid licence, and your own Linux VM (Colima, Lima, UTM) works exactly the same.
Give it the primary’s URI
It has to be a superuser. Schema changes are captured with event triggers, and
CREATE EVENT TRIGGERis superuser-only in PostgreSQL — there is noGRANTfor it. Putting a newly created table into the publication also needs ownership of that table.# what it creates on your primary — all named _snaplicator* _snaplicator_ddl_log table one row per CREATE / ALTER / DROP _snaplicator_capture_ddl trigger writes that row _snaplicator_capture_drop trigger the same, for drops _snaplicator_auto_add_<pub> trigger a new table joins the publication <publication> publication created, or one you already have <slot> slot opened by the replica, as any hasYour rows are read, never written.
wal_level=logicaland the replication role are still yours to set — the installer checks both before it changes anything. Every statement to remove it again is in the README.Pick the tables
The UI lists every table with its size and row count. Nothing is copied until you choose. You can add more later.
Done — replication starts
The first copy is the one slow step, and it happens once. After that the replica follows the primary on its own, rows and schema both.
Create a clone
A writable Postgres on its own port. The reply carries the connection string, and that string is the whole handoff.
create_clone(description: "agent-a / migration-42") → postgres://snaplicator:…@host:5455/appdb
Anonymization is set here. Write it into
configs/anonymize.sqlor edit it from the UI — it runs inside every clone before the port is handed out. If it fails, the clone is destroyed instead of served. If you are not sure how to write it, ask your agent to write it and upload it for you (set_anonymize_sql).UPDATE user_account SET email = 'dev+' || id || '@example.local'; UPDATE device_info SET push_token = 'invalid-dev-token', is_push_enabled = false;
Take a snapshot
A snapshot is a named instant of the replica, taken in O(1) and costing no disk until something writes. Clones start from it and reset back to it, so every run begins on the same data. Take a new one when you want newer data.
create_snapshot(description: "before-migration-42") reset_clone_to_snapshot(clone_id: "5455", snapshot_name: "before-migration-42")
Worth knowing
- Treat the replica as read-only — Postgres will not
- This is a logical replica, not a physical standby, so nothing here rejects a write:
pg_is_in_recovery()is false and the login is a superuser. Write to it anyway and the row is invisible to the primary and never travels back, while the primary’s next change to that row may quietly fail to apply — and anINSERTthat collides with a primary key arriving later stops the apply worker outright. Snapshots carry it with them, so every clone taken afterwards inherits the difference. Write to a clone; that is what a clone is for. - DDL can be deferred, or fail
- Slow DDL such as a large
CREATE INDEXis moved to_snaplicator_ddl_deferred, and the sync loop runs it outside the apply transaction on its own. DDL that errors is written to_snaplicator_ddl_failuresand left there — not retried, not patched over, because a schema you can see is wrong beats one that looks right and is not. Watch for it: add a Slack app to your workspace and paste its incoming-webhook URL into Config, and every DDL apply failure is posted to that channel, with the failing statements. Subscription trouble and schema drift go to the same place. Retrying a failed DDL is still manual — automatic retry is planned.
Hand it to an agent
Register the MCP server and the loop becomes the agent’s own — it creates a clone, works against it, resets it, deletes it. Register it one of two ways, depending on what you are handing over.
Scoped to one clone
For an agent doing a job. It may reset, refresh, snapshot and delete the clones you list, by port — and nothing else. Point it at another clone and the call is refused by name.
// .mcp.json { "mcpServers": { "snaplicator": { "url": "http://snaplicator-host/api/mcp/?clones=5455" } } }
Unscoped
Drop the parameter and nothing is held back: every clone, every snapshot, the replication settings and the primary’s triggers. This is the operator’s connection — yours, not something you hand out.
// .mcp.json { "mcpServers": { "snaplicator": { "url": "http://snaplicator-host/api/mcp/" } } }
The loop
# in the agent's own words create_clone(description: "try #1") → port 5455 … run the migration, run the suite, it fails … reset_clone_to_snapshot(clone_id: "5455", snapshot_name: "before-migration-42") … try #2 … delete_clone(clone_id: "5455")
clones is declared by the client itself, so it stops an accident, not an
attacker. It also only guards the tools that change an existing clone. Creating a new
one is never blocked, and neither is anything aimed at the primary — installing capture
triggers, changing the publication, or replacing anonymize.sql, which decides
what every future clone scrubs.