Seed the Parent Branch, Not Every PR: Neon Branching Meets Seed Data
By Mikhail Shytsko, Founder at Seedfast · · Updated
Neon branches copy their parent in milliseconds, a property that either saves you from the seeding problem entirely or drops a subtler one in your lap. Here's how to tell which situation you're in, and what to do in each.
- Neon branches inherit their parent's data via copy-on-write, so the seeding question is not "how do I seed every branch" but "how do I seed the parent well enough that branches come up populated"
- Seed the parent once and let every preview branch inherit; per-PR seeding turns into an O(1) cost, and most teams stop there
- The pattern breaks only on two kinds of branches, schema-drift PRs (migration adds a
NOT NULLcolumn the inherited rows don't satisfy) and schema-only branches (no data copied at all) - For both exceptions, regenerate the data from the live schema on that branch, since a static
seed.sqlcan't describe the drifted schema and a hand-writtenseed.tshas to change with every migration - Seedfast reads the branch's schema on each run and produces a valid, connected dataset from a plain-English scope, and the same CLI handles parent-seed, drift-reseed, and schema-only-branch without switching tools
Having provisioned Neon for the branching, you get a fresh, full-fat database on every pull request in about a second, copied from the parent branch and ready for CI. The catch is that "copied from the parent" only solves the seeding problem if the parent is actually seeded, and only holds while the branch schema still matches what was in the parent. Three Neon branching seed data patterns cover that full workflow, and this guide walks through all three, parent-once, drift-reseed, and schema-only rescue.
If you haven't read the fundamentals yet, how to seed a Neon database covers the raw SQL, Prisma, and Drizzle mechanics that this article builds on. This one is specifically about how branching changes the seeding model.
Neon's branches are copy-on-write, so when you create a branch from main, the child branch points at the same storage as the parent and no bytes are duplicated until one side writes. Reads against the child see the parent's data instantly, while writes on either side diverge from that moment forward.
That model is the whole reason branching is fast. Because no copying happens up front, Neon's own numbers put branch creation at roughly one second regardless of database size, whether you're inheriting one terabyte or one row.
Two flags at branch creation decide how much you inherit:
- Full branch copies schema and data, the default that preview branches for PRs usually want.
- Schema-only branch copies the schema but leaves the data out, which is the choice when the parent holds sensitive data you can't replicate to every PR, such as production-like staging, a healthcare dataset, or anything under an existing compliance lane.
Full branches make the seeding problem disappear for most PRs, while schema-only branches reintroduce it in a narrower but harder form that most teams eventually run into as well.
The same inheritance now decides what AI agents work against. Neon's branch-per-agent-session pattern spins up a branch for every agent run, and each of those branches is born holding exactly what the parent held, so a seeded parent hands every agent a populated sandbox while an unseeded one hands every agent the same emptiness to improvise against. Which database an agent should touch at all is its own question; on Neon, what the agent finds inside is decided at the parent.
The core insight of Neon branching seed data is that you almost never need to seed a branch at all, so you seed the parent (usually main, dev, or a dedicated seed branch), and every branch created from it comes up populated.
# Seed the parent once, from your local machine or a manual CI job
seedfast connect
seedfast seed --scope "realistic e-commerce: 3 orgs, 200 users, 1,000 orders across 50 products"
Now the preview workflow has no seeding step at all:
# .github/workflows/preview.yml
name: Preview branch per PR
on:
pull_request:
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create Neon branch from main
id: neon
uses: neondatabase/create-branch-action@v6
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch_name: preview/pr-${{ github.event.pull_request.number }}
parent_branch: main
database: ${{ secrets.NEON_DATABASE_NAME }}
role: ${{ secrets.NEON_ROLE_NAME }}
api_key: ${{ secrets.NEON_API_KEY }}
- name: Run migrations on the new branch
run: npx prisma migrate deploy
env:
DIRECT_URL: ${{ steps.neon.outputs.db_url }}
- name: Run E2E tests
run: npm run test:e2e
env:
DATABASE_URL: ${{ steps.neon.outputs.db_url_pooled }}
Specify the database and role inputs when your Neon project uses non-default names. Without them the action falls back to neondb / neondb_owner, and if those don't exist on your project, the output DSNs point at credentials Postgres will refuse to connect with, the kind of silent misconfiguration that shows up only when the test step runs. Otherwise there's no seed step at all, because the branch is already populated; a typical preview pipeline runs in the time it takes to deploy the app, and the database ends up being the cheap part of it.
This pattern works until either a PR introduces a migration that changes the schema in ways the inherited rows can't satisfy, or your security review forces schema-only branching. Both cases are the rest of this article.
A developer opens a PR that adds ALTER TABLE orders ADD COLUMN fulfillment_provider TEXT NOT NULL, the branch gets created from main, and the 1,000 inherited orders have no fulfillment_provider to satisfy it. The migration fails outright, or worse, it doesn't, because someone added a default, and now your tests are running against orders where every provider is the same literal string.
You don't want to reseed every branch (that defeats the O(1) advantage); you want to reseed only the branches where the inherited data no longer matches what the schema expects. A simple heuristic is "does this PR touch the migrations directory?"
# .github/workflows/preview.yml (excerpt)
- name: Check if PR changes migrations
id: migrations
uses: tj-actions/changed-files@v46
with:
# Adjust the glob list to match your migrator. Cover every location
# where schema changes can land — Prisma, Drizzle, raw SQL, Flyway,
# Liquibase, custom folders. A missed path here means CI silently
# runs against stale inherited data.
files: |
prisma/migrations/**
drizzle/migrations/**
db/migrations/**
supabase/migrations/**
**/*.sql
- name: Run migrations on the new branch
run: npx prisma migrate deploy
env:
DIRECT_URL: ${{ steps.neon.outputs.db_url }}
- name: Reseed branch for schema-drift PRs
if: steps.migrations.outputs.any_changed == 'true'
run: npx seedfast seed --scope "e2e baseline: 3 orgs, 20 users, 100 orders" --output json
env:
SEEDFAST_API_KEY: ${{ secrets.SEEDFAST_API_KEY }}
SEEDFAST_DSN: ${{ steps.neon.outputs.db_url }}
The reseed step only runs on branches that changed a migration file. The glob list above is the most brittle part of the workflow, because if your team commits migrations somewhere it doesn't cover, CI silently runs against stale inherited data and green builds can ship breaking schema changes. Treat the list as a contract with your migrator and update it whenever that surface changes. The Seedfast CLI flags used here (--scope, --output, SEEDFAST_API_KEY, SEEDFAST_DSN) are covered in the CI/CD database seeding docs. Seedfast has a 30-day free trial that covers small schemas end to end, so connect your Neon project and run the Pattern 2 step on a test PR before wiring it into the real workflow.
Two failure modes the glob heuristic does not catch: (a) business-logic PRs that need different data even though no schema changed, which need a manual reseed trigger on the parent branch; (b) schema changes that happen via side effects (e.g., a PR that runs a one-off SQL script from application code rather than a migration file). For (b), the only durable fix is "all schema changes go through the migrator," which is a team discipline issue, not a CI one.
This is the pattern where a static seed.sql stops working. The file describes the old schema, the one that existed in main, while the new branch's schema carries a new column, a new table, or a renamed foreign key the file knows nothing about. Nobody updates it until something breaks, and that job usually falls to whoever is trying to get their own PR to green. What separates reseeding-on-drift from rewriting the seed file inside the PR is that the generator adapts to the schema it finds at run time instead of assuming yesterday's shape. Why static seed files break covers the lifecycle in full; on Neon, the drift hits earlier because you feel every migration on every preview branch.
Schema-only branching is Neon's answer to "we can't copy that data to every PR." The parent database might hold records covered by a regulatory scope your team can't replicate to every developer's preview, or sensitive payment metadata that staging can't expose outside its access boundary; sometimes the dataset is simply too large to copy a hundred times a day regardless of what it contains. Schema-only branches inherit the structure (tables, columns, indexes, foreign keys) and nothing else. Seedfast itself generates synthetic rows and holds no compliance certification of its own; the fit here is that synthetic data replaces the copy, and whatever compliance posture your team already operates continues to apply.
That leaves you at an empty database with a real, complex schema in front of it, the seeding problem in its purest form.
The traditional answers are bad in this specific case:
- A committed
seed.sqlgoes out of date the day after the next migration lands, and doubly so on a schema-only branch where no inherited rows mask the gap. See seed file maintenance for the full argument. - A Prisma or Drizzle
seed.tsdoes better than raw SQL, but it's still hand-written against a schema that keeps changing under it, so every new FK becomes a hand-edit. - Copy-and-anonymize from production is the right answer for some teams, but it's heavy, since you end up running a full data pipeline just to fill a CI database.
- Faker libraries generate values but not relationships, so a 20-table schema becomes 20 one-table seed scripts with manual FK wiring, a pattern that stops scaling around table ten on any real schema.
Seedfast reads the branch's live schema on each run and generates rows that fit it, valid and connected, with FKs pointing at real parents. You describe the dataset in a plain-English scope:
# On the schema-only branch
export DIRECT_URL="postgresql://...ep-xxxx.region.aws.neon.tech/db?sslmode=require"
seedfast seed --scope \
"small SaaS org: 2 workspaces, 15 members, 100 projects with 500 tasks and activity history"
There's no seed.sql and no seed.ts. The CLI connects, discovers what the last migration added, and generates valid, connected rows regardless. If the schema changes next week, the same command works next week. Self-referential tables (employees.manager_id → employees.id) and looped foreign-key chains are handled without any special-casing on your side.
The same binary that handles your parent-branch seed and your drift-reseed also handles schema-only. That's the reason most teams want one tool covering the full workflow instead of three different ones duct-taped together.
Seedfast is meant to coexist with the small set of deterministic fixtures most apps rely on, such as the admin@example.com account your Playwright login spec types, the feature-flag rows keyed by key, or the country-code lookup table. Keep those in a short fixtures.sql or a trimmed-down seed.ts and run them after seedfast seed. Seedfast fills the bulk relational data around them; your fixture file pins the specific records your tests expect by literal value. The two layers compose cleanly because Seedfast writes rows through the same Postgres connection your fixture script uses, so there's no tool-specific state to reconcile. Trying Seedfast against any Neon connection string is free for the first 30 days, and you can run your first seed in about two minutes.
Sometimes a branch gets polluted mid-test, whether a destructive migration runs, a test accidentally writes real data, or a developer just wants to start over. Neon supports resetting a child branch to its parent, a one-call operation via the API or dashboard that drops the branch's divergent writes and restores parent state.
# Restore a branch to the latest state of its parent (main)
curl --request POST \
--url "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/restore" \
--header "Authorization: Bearer $NEON_API_KEY" \
--header "Content-Type: application/json" \
--data '{"source_branch_id": "'"$PARENT_BRANCH_ID"'"}'
The reset takes about a second for the same reason branch creation does, since no data is copied, just a pointer update. That makes "reset to parent, rerun tests" a tight feedback loop when you're debugging flaky tests that might be polluting each other. Combined with Pattern 1 (parent already seeded), you get an instant clean environment every time.
One caveat is that reset-to-parent restores the branch's state to the parent's current state, which means if the parent has not had the PR's migration applied (it usually hasn't), the next CI run will re-apply the migration and overwrite whatever you reset to. The loop is "clean environment for test-data pollution," not "clean environment for migration rollback." For migration-drift branches in Pattern 2, the correct recovery is to delete and recreate the branch rather than reset, since reset will immediately be undone by the next prisma migrate deploy step.
If the parent itself is drifting out of shape (say, main has ten-month-old orders that don't reflect the current product schema), the fix is reseeding the parent, not resetting branches. seedfast seed is idempotent against a freshly-truncated schema, so the operational pattern is to truncate the target tables, then run seedfast seed with the current scope.
Neon bills compute seconds per branch, and a branch that runs a seed for the length of the seed is billing the whole time it runs. For a concrete team, the number depends on how long your seed actually takes: a short reference-data seed takes seconds, while a realistic-volume seed with FK chains takes minutes. What stays fixed is the shape of the bill. Seed every PR, and the line scales linearly with PR volume.
Seeding the parent once and letting branches inherit replaces that line with near-zero CI compute on most PRs. The parent-seed compute happens once after each schema change on main, usually as a scheduled job or a manual workflow dispatch, and it doesn't run for every PR that opens. Preview branches pay the compute cost of their actual tests, nothing more. If your team moves fast on migrations, "once after each schema change" still means a handful of parent reseeds per week, but each one replaces dozens of per-branch seeds that would otherwise run on every PR that opened during the same week.
When schema drift forces a reseed on a specific branch, you pay one seed's worth of compute for that one branch. That's the right bill; you asked the database to do work because the schema changed. The only real waste is paying for that seed on every PR, including the 90% that never touched migrations, and the parent-once pattern is what eliminates that line entirely.
Neon branch caps. Every Neon plan has a branch limit (Free plans are tighter than paid), and PRs that sit open for weeks quietly accumulate branches against it. The delete-branch-action on pull_request: closed is required, not optional, because skipping it means you'll hit the cap mid-PR while the preview workflow fails at branch creation.
Parallel test isolation. If you run Jest or Playwright in parallel and all workers hit the same preview branch, they race for the same rows. For true isolation, create one branch per worker at the start of the test run and delete them at the end. Six workers with one-second branch creation add up to six seconds of overhead, which is vastly cheaper than running six separate seeded databases.
Pooled vs unpooled for seeding. The parent-seed and drift-reseed jobs need the unpooled connection string, because PgBouncer in transaction mode breaks prepared statements and can time out on large seeds, while the app under test uses the pooled string instead. This is the same rule as non-branching Neon seeding; the Neon seeding guide covers the connection-string mechanics in detail.
Auto-suspend mid-seed. Neon computes suspend after an inactivity window; the default is 5 minutes on all plans, and the Free plan cannot disable it. A seed that pauses between large batches can have its connection closed underneath it, surfacing as server closed the connection unexpectedly or a generic TCP reset. Paid plans can raise the suspend delay for the seed branch; Free-plan users need the seed to run continuously without long inter-batch pauses. seedfast seed runs one continuous session, so this mainly bites hand-rolled scripts that batch-and-wait.
Stale parent. The parent-seed pattern assumes the parent's data still reflects the current schema. When a migration lands on main, either (a) run the migration against the parent branch's compute and re-run the parent seed to regenerate rows that fit, or (b) accept that the parent will drift and rely on Pattern 2 (drift-reseed on every PR that touches migrations). Most teams pick (a) as a post-merge job on main.
For the practical setup, the article covers three workflows and picks between them based on the PR:
| Scenario | Pattern | What happens in CI |
|---|---|---|
| PR touches app code only | Inherit from parent | Create branch, run migrations (no-op), run tests |
| PR touches migrations | Drift reseed | Create branch, run migrations, run Seedfast on the branch, run tests |
| Branch is schema-only (compliance scope) | Schema-only rescue | Create schema-only branch, run migrations, run Seedfast, run tests |
All three use the same seedfast binary with different scope strings; the difference is when you invoke it, not how. That's the operational simplification that makes Neon branching seed data manageable at team scale, one binary and one mental model standing in for what used to be three separate tools.
Do Neon branches inherit seed data from the parent?
Yes, by default. A full branch copies both schema and data from the parent through copy-on-write, so the preview branch is populated the instant it's created, with no separate seeding step required. Schema-only branches are the exception, since they copy the table structure but skip the data, so those branches come up empty and need seeding on their own.
Should I seed every Neon branch or just the parent?
Seed the parent. Preview branches inherit the data, which keeps CI time and Neon compute cost flat regardless of how many PRs you have open. Reseed a branch only when something has changed that the inherited data can't represent, typically a new migration introducing columns, tables, or FK changes.
How do I populate a schema-only Neon branch?
Run the seeder against the branch's direct connection string after migrations. Static seed.sql breaks fast here, since the file rarely stays current with the schema a schema-only branch actually has. Seedfast generates rows for whichever tables and columns the branch actually has, so seedfast seed --scope "..." works exactly the same way it does on a full branch; it just has more empty tables to fill.
Can I reset a Neon branch back to parent state mid-test?
Yes. The Neon API's restore endpoint (POST /api/v2/projects/{project_id}/branches/{branch_id}/restore with a source_branch_id body) drops divergent writes and restores the parent's state in about a second. The tight feedback loop is useful when debugging flaky tests or accidentally polluting a preview branch.
How do I seed a Neon preview branch in GitHub Actions?
If the parent is already seeded, you don't, because the branch inherits the data when neondatabase/create-branch-action runs. If the PR changes the schema, detect the migration change (e.g., with tj-actions/changed-files) and run seedfast seed only on those branches. Use the unpooled (db_url) output from the action for the seed step instead of the pooled URL.
What happens if I hit Neon's branch limit?
Branch creation in CI fails. Pair create-branch-action on pull_request: opened with delete-branch-action on pull_request: closed in the same workflow repo. For extra safety, add a scheduled cleanup that deletes branches older than N days whose PR is closed, since stale preview branches are the usual culprit.
Does this pattern work with Prisma / Drizzle / Kysely migrations?
Yes. The create-branch → run-migrations → (optional seed) → run-tests pipeline is ORM-agnostic. Prisma uses prisma migrate deploy, Drizzle uses drizzle-kit migrate to apply committed migration files (use drizzle-kit push only for local schema prototyping, never in CI), and Kysely uses whatever migrator you've wired up. seedfast talks to Postgres directly and doesn't care which ORM manages the schema.
Can I keep my existing seed.ts for fixture records and use Seedfast for bulk data?
Yes, and most teams do. Seedfast handles the bulk relational data (orders, events, activity history) that makes tests realistic but that nobody wants to hand-write, while the handful of records a test asserts on by exact value stay in your existing seed.ts or a short fixtures.sql. Run Seedfast first for volume, then the fixture file for specifics; the two share the same Postgres connection and never step on each other.
Is the data Seedfast generates reproducible across runs?
Not by default, since the generator produces fresh values on each run, which is what you want when the goal is a realistic dataset. Tests that depend on specific literal values (an email, a product name) should stay in a fixture file that inserts those rows deterministically. For E2E baselines that need the exact same dataset every run, the standard pattern is to run Seedfast once, pg_dump the result, and restore the dump in CI, the same approach any generator-based workflow uses.
- How to seed a Neon database, covering the three seeding methods (raw SQL, ORM, schema-aware) this article builds on, including connection-string gotchas and per-error fixes
- How to seed a Supabase database, the Supabase counterpart covering
seed.sql,supabase db reset,auth.users, and Supabase preview branches (separate-DB-per-branch model, not CoW) - Seed file maintenance, on why static
seed.sqland hand-writtenseed.tsdrift from the schema, and how that drift shows up faster on Neon preview branches - Database seeding in CI/CD, the framework for idempotent, fast, FK-valid seeding in a pipeline, applied to any Postgres (Neon-specific here; this covers the general case)
- Database seeder tools and per-ORM reference, comparing Laravel, Prisma, Drizzle, EF Core, and TypeORM seeders side-by-side
- Get started with Seedfast, to connect the CLI to your Neon project and run the three patterns in under five minutes