Database Branching for Preview Environments
Database branching gives each feature branch or pull request its own copy of the database, so a preview deployment can run schema changes and tests without touching production or a shared staging database. A branch is created when the pull request opens, gets the pull request's migrations applied, and is deleted when it closes. The result is the same workflow you already have for code: isolated, disposable, reviewable.
This article compares the ways branching is implemented, explains the data question (empty, seeded or masked), and walks through a workflow with GitHub.
#The problem branching solves
Without branches, teams usually share one staging database. That breaks in predictable ways:
- Two pull requests change the same table differently, and staging ends up in a state neither expects.
- A migration that works on an empty dev database fails on staging because of real data.
- Someone tests a destructive migration and everyone's test data is gone.
- Preview deployments of the frontend all point at the same backend, so you cannot review a schema change in isolation.
A branch per pull request removes the sharing.
#Three ways to implement database branching
| Approach | How it works | Strengths | Weaknesses |
|---|---|---|---|
| Copy-on-write storage | The branch shares storage pages with its parent and only stores what changes | Instant, any size, includes data | Needs a storage engine built for it; branch data is real production data unless masked |
| Rebuild from migrations | A new database is created, migrations run in order, then a seed script | Proves your migrations work from scratch; no production data by default | Slower for long migration histories; seed data must be maintained |
| Snapshot restore | A backup or snapshot is restored into a new instance | Real data, standard tooling | Slow and costly for large databases; real personal data |
Neon is the best-known example of copy-on-write branching. StitchDB uses the second approach for preview branches (rebuild from migrations, optionally with masked production data) and offers the third through point-in-time restore into a new branch.
Each approach is valid. The rebuild approach has an underrated benefit: if your migrations do not run cleanly from zero, you find out in the pull request, not when you set up a new environment six months later.
#What a branch should include
A database alone is often not enough for a real preview. Ask what your preview deployment talks to:
- The database, with the branch's migrations.
- API URL and keys of its own, so the preview frontend can point at it.
- Auth with its own users, so testers can sign up without polluting production.
- Storage buckets of its own.
- Functions deployed from the branch.
On StitchDB, a preview branch is a separate project that belongs to the production project: its own Postgres database, API URL, keys, Auth users, storage and functions. Auth settings (providers, redirect URLs, e-mails) are copied from production. Idle branches pause after 3 days without requests and are restored from their page. See Preview branches.
#The data question
What should be in a branch database? Three options, from safest to most realistic.
#1. Schema plus seed data
The branch has your schema and a seed script with test fixtures. This is safe, fast and reproducible. The downside is that seed data never looks like production: no edge cases, no volume.
Keep seed.sql small and deterministic:
insert into public.plans (id, name, price_cents) values
('free', 'Free', 0), ('pro', 'Pro', 1599)
on conflict do nothing;#2. Masked production data
The branch gets production's rows with personal data replaced. This catches the "works on empty tables, breaks on real data" class of bugs without spreading real e-mails and phone numbers into test environments.
On StitchDB, choose Copy production data, masked when creating a branch (or --with-data on the CLI). Masking runs inside production's own query, so real values never leave production. Columns are masked by name and type: e-mails, phone numbers, names, addresses, IP addresses, birth dates (shifted), and tokens and passwords (removed). The same value gets the same fake value everywhere in one copy, so joins and unique keys keep working. You can add rules in SQL so they live in your migrations:
comment on column public.tickets.body is '@mask text';
comment on column public.orders.gift_note is '@mask null';Masking is a safeguard, not legal anonymization: free-text and JSON columns can still contain personal data, so review them. The copy size is limited by plan (1 GB on Pro, 2 GB on Scale and Enterprise as of November 2026).
#3. A point-in-time copy
For debugging a production incident, a branch with production's data at a specific moment is useful. On StitchDB, Backups › Restore to a point in time › A new branch creates one with its own URL and keys. Treat it as sensitive: it contains real data.
#A pull request workflow
Here is the flow most teams want:
- A developer opens a pull request with a new migration.
- A branch is created for the pull request, the migrations run, and a comment links the branch URL.
- The preview frontend deploys with the branch's API URL and keys.
- Reviewers test the feature against the branch.
- On merge, the migrations apply to production, and the branch is deleted.
#With the StitchDB GitHub App
Install the app from Integrations › GitHub, link the repository, pick the production branch and the folder that holds migrations/. From then on, every pull request gets a preview branch with stitchdb/migrations/*.sql applied, a StitchDB preview check, and a comment with the branch's API URL. If a migration fails, the check fails with the Postgres error. When the pull request is merged or closed, the branch is deleted. Optionally, pushes to the production branch apply new migrations to production. See GitHub integration.
#With GitHub Actions or another CI
If you prefer a workflow file:
stitchdb init --githubThis writes .github/workflows/stitchdb.yml. Add a STITCHDB_ACCESS_TOKEN secret and a STITCHDB_PROJECT_REF variable. The workflow creates a branch per pull request, pushes migrations and deploys functions, comments the URL, deletes the branch on close, and pushes migrations to production on merge to main. Use either the GitHub App's apply-on-push or the workflow, not both, so migrations do not run twice. See CI.
#From the command line
stitchdb branches create feature/checkout --wait
stitchdb branches switch feature/checkout # db push, functions deploy, gen types now target the branch
stitchdb db push
stitchdb branches merge feature/checkout --dry-run
stitchdb branches merge feature/checkout --delete
stitchdb branches switch main#Merging: schema, not data
Merging a branch should apply its schema changes to production. It should not copy branch data into production; test data does not belong there.
On StitchDB, merging applies the migrations the branch has and production does not, in order, on production. Data is not merged.
This puts the weight on your migrations. A few habits help:
- Keep migrations additive. Add columns and tables, backfill, then remove old ones in a later migration. A merge then never blocks production for long.
- Avoid long locks. Use
create index concurrentlyandnot validconstraints. StitchDB'sstitchdb db pushwarns about statements that lock busy tables and suggests safer versions; see Safe schema changes. - Never edit an applied migration. Put changes in a new file.
- Rebase migrations. If two pull requests add migrations, the one merged second may need its timestamp updated so it runs after the first.
#Costs and limits
Branches are real databases, so they count against something. On StitchDB:
| Item | Detail |
|---|---|
| Plans | Pro includes 3 branches per project; Scale and Enterprise include more. Not on Free |
| Usage | Branches count toward the account's database and file quota |
| Idle branches | Pause after 3 days without requests; not deleted for inactivity |
| Limit reached | The GitHub check fails with the reason and no branch is created |
Delete branches when pull requests close (the GitHub App and workflow do this) and the limit is rarely a problem.
#When branching is not worth it
For a solo developer with a small schema, a local database (stitchdb start runs the stack locally) and a single staging project may be all you need. Branching pays off when several people change the schema in parallel, or when frontend previews need a matching backend.
For more on keeping schema changes safe, see Postgres Backup Best Practices and the migrations docs.
#FAQ
#Is database branching the same as a staging environment?
No. Staging is one shared environment. Branching gives each pull request its own short-lived environment.
#Do branches include production data?
On StitchDB, only if you ask: masked production data, or a point-in-time copy. By default a branch has your migrations and seed script.
#What happens if a migration fails on the branch?
The check on the pull request fails with the Postgres error and the file name. Fix the migration and push again.
#Can I branch on the Free plan?
Not on StitchDB. Preview branches are on Pro and up. Free projects can run the stack locally with the CLI.
Want a branch for every pull request? See plans with branching or create a project.