How to Seed a Neon Database Without Hating Your seed.sql
By the Seedfast team ·
Seed Neon database in one command — or keep rewriting seed.sql every time the schema changes. Neon gives you an empty serverless Postgres in under a second; then you stare at it. Here are three ways to fill it.
Key Takeaways#
- Neon's main branch starts empty — every new project, every new branch, every preview environment needs test data before the app is usable
- The simplest path is
psql "$NEON_DATABASE_URL" -f seed.sql, but use the unpooled connection string for seeding; PgBouncer transaction mode breaks prepared statements and long-running scripts - Prisma and Drizzle both seed Neon fine, but Prisma on edge runtimes requires the
@prisma/adapter-neonadapter plusws; the standardpgdriver won't work there - Neon branching inherits data from the parent branch, so seeding once at the parent lets preview branches start with that dataset, ready in about a second
- When the schema changes — which it will — static seed files break. Seedfast reads Neon's live schema on every run and regenerates valid data, so there is no seed file to maintain
You provisioned Neon because it spins up in 5ms, scales to zero, and lets you branch databases per pull request. But your main branch is empty, and your CI branches inherit that emptiness. To seed a Neon database, you need three things: the right connection string, a strategy that survives schema changes, and an understanding of how Neon branching changes the seeding model.
This guide covers all three. We'll start with raw SQL, walk through Prisma and Drizzle seed scripts with Neon's serverless driver, then show how Seedfast reads your schema and generates connected data — no seed file required. If you want the general PostgreSQL version first, how to seed a database covers the cross-framework fundamentals; this article is specifically about Neon.
Why a Neon database needs seeding#
Neon's serverless Postgres starts life as an empty database. Unlike a shared dev server that accumulates data over months, a Neon project is fresh. Branches are fresh too — by default they copy from a parent, so if the parent is empty, the branch is empty. This matters more on Neon than on traditional Postgres hosting because the branching workflow puts a new database in front of you on every pull request.
Three scenarios force the question of how to seed a Neon database:
- New project onboarding. A developer clones the repo, creates a Neon project, runs migrations. Tables exist. Nothing else does.
- Preview branches per PR. The GitHub Action creates a branch for every pull request. If the parent is empty, every preview is empty. Your end-to-end tests hit 404s.
- Staging and demo environments. You need 500 products and realistic order histories to show someone what the app looks like with data in it.
Neon's own documentation pushes branching over seeding — branch from a parent that already has data, and every child branch inherits it. That's elegant, but it leaves the parent-seeding problem unsolved. Someone still has to fill the parent branch the first time.
Connection strings: pooled, unpooled, and when each matters#
Neon gives every branch two connection strings. You can copy them from the Neon dashboard under Connection Details:
# Pooled (goes through PgBouncer, up to 10,000 concurrent connections)
postgresql://user:pass@ep-xxxx-pooler.region.aws.neon.tech/dbname?sslmode=require
# Unpooled / direct (straight to Postgres)
postgresql://user:pass@ep-xxxx.region.aws.neon.tech/dbname?sslmode=require
Note the -pooler in the pooled hostname. That's PgBouncer in transaction mode — connections get returned to the pool after every transaction. It's what you want for a serverless Next.js app handling thousands of short requests. It's not what you want for seeding, because:
- Prepared statements don't survive the transaction boundary
- Long-running
COPYoperations and large transactional seeds can hit statement timeouts - Some ORMs emit session-level
SETstatements that get discarded
For seeding, migrations, and any admin script, use the unpooled string. Export it explicitly:
# .env.local
DATABASE_URL="postgresql://...ep-xxxx-pooler.../dbname?sslmode=require" # app runtime
DIRECT_URL="postgresql://...ep-xxxx.../dbname?sslmode=require" # migrations + seeds
Prisma calls this directUrl in schema.prisma. Drizzle doesn't care — just pass whichever URL matches your context. If your seed inserts 10,000 rows and fails halfway with "prepared statement already exists" or "cached plan must not change result type", you're seeding through the pooler. Switch to the unpooled URL.
Method 1: Seed Neon database with raw SQL#
The simplest thing that works. Write INSERTs, run them with psql.
-- seed.sql
INSERT INTO teams (id, name) VALUES
(1, 'Engineering'),
(2, 'Design')
ON CONFLICT (id) DO NOTHING;
INSERT INTO users (id, email, team_id) VALUES
(1, 'alice@example.com', 1),
(2, 'bob@example.com', 2)
ON CONFLICT (id) DO NOTHING;
psql "$DIRECT_URL" -f seed.sql
SSL is mandatory on Neon — the sslmode=require query parameter in the connection string handles it automatically. If you get FATAL: connection requires SSL, your URL is missing it.
When you have tens of thousands of rows to load, COPY FROM STDIN is substantially faster than row-by-row INSERTs — the gap narrows when comparing against batched multi-row INSERTs, but COPY still avoids per-row parsing overhead:
psql "$DIRECT_URL" -c "COPY products (name, price, category_id) FROM STDIN CSV" < products.csv
ON CONFLICT DO NOTHING keeps the seed idempotent so CI reruns don't fail on the second attempt. For data that should reflect the latest values, use ON CONFLICT DO UPDATE:
INSERT INTO feature_flags (key, enabled) VALUES
('new_checkout', true)
ON CONFLICT (key) DO UPDATE SET enabled = EXCLUDED.enabled;
Raw SQL is fine for reference data — roles, feature flags, country codes. It starts breaking when you have more than ten tables with foreign keys, because every migration that adds a required column or a new FK reference forces you to hand-edit the seed file. Seed file maintenance covers why this lifecycle is so brutal on active codebases.
Method 2: Seed Neon database with an ORM#
Most Neon projects run through Prisma, Drizzle, or Kysely. Each has its own seeding path.
Prisma + Neon#
Prisma on a Node.js server works with Neon out of the box via the standard pg driver. Edge runtimes (Vercel Edge, Cloudflare Workers) require the Neon serverless driver adapter.
In a regular Node.js seed script, just point Prisma at the unpooled URL:
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const team = await prisma.team.upsert({
where: { name: 'Engineering' },
update: {},
create: { name: 'Engineering' },
});
await prisma.user.upsert({
where: { email: 'alice@example.com' },
update: {},
create: { email: 'alice@example.com', teamId: team.id },
});
}
main().catch(console.error).finally(() => prisma.$disconnect());
// prisma.config.ts
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
seed: 'tsx prisma/seed.ts',
},
datasource: { url: env('DIRECT_URL') },
});
npx prisma db seed
If you deploy on Vercel Edge or Cloudflare Workers, the runtime has no TCP sockets. Install the adapter:
npm install @prisma/adapter-neon @neondatabase/serverless ws
import { PrismaClient } from '@prisma/client';
import { PrismaNeon } from '@prisma/adapter-neon';
import { Pool, neonConfig } from '@neondatabase/serverless';
import ws from 'ws';
neonConfig.webSocketConstructor = ws;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaNeon(pool);
const prisma = new PrismaClient({ adapter });
Seeding itself rarely runs on edge — it runs in CI or locally — so the plain Node.js setup with the unpooled URL is what most teams use for the seed script.
Drizzle + Neon#
Drizzle ships two Neon-specific packages. drizzle-orm/neon-http for one-shot HTTP queries (great for serverless app code, not for seed transactions). drizzle-orm/neon-serverless for WebSocket sessions with full transaction support (what you want for seeds).
The simplest seed path is the node-postgres driver against the unpooled URL:
// scripts/seed.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { teams, users } from '../src/db/schema';
const pool = new Pool({ connectionString: process.env.DIRECT_URL });
const db = drizzle(pool);
async function seed() {
const [team] = await db
.insert(teams)
.values({ name: 'Engineering' })
.onConflictDoUpdate({ target: teams.name, set: { name: 'Engineering' } })
.returning();
await db
.insert(users)
.values({ email: 'alice@example.com', teamId: team.id })
.onConflictDoNothing();
await pool.end();
}
seed().catch((e) => {
console.error(e);
process.exit(1);
});
tsx scripts/seed.ts
If you prefer the Neon serverless driver for consistency with the rest of your codebase:
import { drizzle } from 'drizzle-orm/neon-serverless';
import { Pool, neonConfig } from '@neondatabase/serverless';
import ws from 'ws';
neonConfig.webSocketConstructor = ws;
const pool = new Pool({ connectionString: process.env.DIRECT_URL });
const db = drizzle(pool);
Either works. The pg version is one less dependency; the serverless-driver version keeps your app and seed script on the same driver.
Both Prisma and Drizzle share the ORM seed problem: the values are hand-written. When a migration adds a NOT NULL organization_id column, your seed breaks on the next run, and someone — usually the person on call — has to fix it before anyone on the team can run the app.
Method 3: Schema-aware seeding with Seedfast#
Seedfast connects to your Neon branch, reads the live schema — tables, columns, constraints, foreign keys — and generates a valid, connected dataset. You describe what the data should look like in plain English. There's no seed.sql or seed.ts to maintain.
npm install -g seedfast
# or: brew install argon-it/tap/seedfast
# Log in and connect to your Neon database
seedfast connect
# Paste the Neon unpooled connection string when prompted
# Generate data from the current schema
seedfast seed --scope "small engineering team with 3 projects and task assignments"
When a migration adds a new table or column, the next seedfast seed picks it up. No file to update. No foreign key order to think about — Seedfast builds the dependency graph from the schema and inserts in topological order.
Different scopes for different environments:
# Local dev — minimal, fast
seedfast seed --scope "2 teams, 5 users, 10 products"
# Preview branch for a feature PR
seedfast seed --scope "3 users with completed onboarding, 5 draft posts, 2 published"
# Staging demo
seedfast seed --scope "500 realistic products, 50 users with 6 months of order history"
Seedfast works alongside Prisma, Drizzle, Kysely, and plain pg because it talks to PostgreSQL directly over the wire. Run your migrations first with whichever tool you prefer, then run Seedfast to fill the tables.
For production reference data (feature flags, country codes, admin roles) you still want a versioned SQL file or ORM seed — that data belongs to the application, not a test dataset. Seedfast is for development, CI, staging, and demo data, not for the three rows that ship to production.
Generated rows are written straight into your Neon branch — no CSV dump to import, no intermediate file to load, no manual psql step at the end. One command, and the tables are full. See data handling and privacy for exactly what crosses the wire.
A free trial is available — connect and run your first seed in about two minutes.
Seed Neon once, branch many times#
This is where Neon's branching model and seeding become a superpower. Branches inherit their parent's data — you seed the parent once, and every branch forked from it starts with that dataset, available in milliseconds.
A typical workflow looks like:
# One-time setup on main branch
psql "$DIRECT_URL" -f seed.sql
# or
seedfast seed --scope "realistic e-commerce dataset"
Then in CI, every pull request gets its own branch:
# .github/workflows/preview.yml
name: Preview branch
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
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 }}
No seeding step in CI. The branch already has data because it inherited it from main. Branch creation takes about a second, so the PR pipeline isn't waiting on database provisioning.
When the PR is closed or merged, a cleanup action deletes the branch:
# .github/workflows/cleanup.yml
on:
pull_request:
types: [closed]
jobs:
delete-branch:
runs-on: ubuntu-latest
steps:
- uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch: preview/pr-${{ github.event.pull_request.number }}
api_key: ${{ secrets.NEON_API_KEY }}
The two pieces fit together: seed the parent with a realistic dataset, then branch per PR for fast, isolated preview environments. The only thing you have to get right is keeping the parent's seed data current — and that's exactly where a schema-aware seeder earns its keep.
For the general CI case without branching, the approach in CI/CD database seeding applies to Neon too.
Common Neon seeding issues and how to fix them#
"prepared statement s1 already exists"#
You're seeding through the pooled (-pooler) connection string. PgBouncer transaction mode discards prepared statements between transactions, and the driver tries to reuse a statement that's gone. Switch to the unpooled URL for the seed script.
"cached plan must not change result type"#
A server-side Postgres plan cache error: the database cached the execution plan for a prepared statement, but a subsequent schema change (column type, table restructure) invalidated it. Run migrations before the seed, and make sure you are using the unpooled URL — PgBouncer can mask this error by discarding statement state between transactions rather than surfacing it cleanly.
"connection terminated unexpectedly" mid-seed#
Neon compute auto-suspends branches that have been idle (default is 5 minutes on all plans, including paid). If your seed script pauses between large batches, the connection may close before you resume. For long seeds, keep the script running continuously or increase the compute's suspend delay in the Neon dashboard.
"SSL required"#
Your connection string is missing ?sslmode=require. Neon rejects non-SSL connections. Add the query parameter or set PGSSLMODE=require in the environment.
"permission denied for schema public"#
Neon projects have one default role: the owner role (named for your database, e.g., neondb_owner), which has full access. If you are using a role you created separately with limited grants, your seed INSERTs will fail. Use the project owner role for seeding.
"too many connections"#
You've opened more connections than the compute allows — the limit scales with compute size (a 0.25 CU Neon compute supports around 104 total connections; a 1 CU supports 419). Close pools after seeding (pool.end() for pg / Drizzle, prisma.$disconnect() for Prisma). For parallel seed scripts, serialize them or lower the pool size (max: 5 in new Pool(...)). Use the direct URL for the seed process only.
"relation does not exist"#
Run migrations before seeding. Neon branches copy data from the parent, but if the parent hasn't had migrations applied, the schema is stale. The order is always migrate → seed, never the reverse.
Manual SQL vs ORM vs Seedfast for Neon#
| Aspect | Raw SQL (psql -f) | Prisma / Drizzle seed | Seedfast |
|---|---|---|---|
| Setup time | None | Already there if you use the ORM | npm install -g seedfast |
| File you maintain | seed.sql | seed.ts | None — reads the live schema |
| FK order | Manual | Manual | Automatic |
| Survives migrations | No — requires manual updates on schema changes | No — requires manual updates on schema changes | Yes — regenerates from the live schema |
| Realistic volumes | Painful beyond ~50 rows | Works with Faker, still manual | Natural-language scope describes the dataset |
| Works with Neon branches | Yes (unpooled URL) | Yes (unpooled URL) | Yes (unpooled URL) |
| Good for static config (feature flags, country codes, roles) | Excellent — versioned and reviewed | Excellent — versioned and reviewed | Not the target |
| Good for dev / CI / staging datasets | Manual re-sync on every migration | Manual re-sync on every migration | Regenerates from the live schema |
Pick based on the job. Ship reference data as committed SQL or ORM seed. Use Seedfast for the large, evolving test datasets that are the actual source of seed-file pain — try it free on your Neon schema, no credit card, two-minute setup.
Frequently asked questions#
How do I seed a Neon database from the command line?#
Copy the unpooled connection string from the Neon dashboard (the hostname without -pooler) and run psql "$DATABASE_URL" -f seed.sql. Include ?sslmode=require on the connection string. For Prisma projects, npx prisma db seed runs your prisma/seed.ts. For schemas with many tables or foreign keys, seedfast seed --scope "..." generates connected data without a seed file.
Should I use the pooled or unpooled Neon URL for seeding?#
Use the unpooled URL for seeding, migrations, and admin scripts. The pooled URL routes through PgBouncer in transaction mode, which breaks prepared statements and can time out on large transactions. Use the pooled URL for your application at runtime, where short-lived transactions benefit from the pool.
Do Neon branches inherit seed data from the parent?#
Yes by default. When you create a branch, Neon copies both the schema and the data from the parent branch. That means you can seed your main branch once and every preview branch forked from it starts with that dataset. Neon also supports schema-only branching if you want the structure without the data.
How do I seed a Neon database in GitHub Actions?#
Create the branch with neondatabase/create-branch-action, run migrations against the branch's direct URL, then run your seed script. If the parent branch is already seeded, you can skip the seeding step — the branch inherits the data. Use the pooled URL for application queries in your tests and the direct URL for migrations and seeds.
How do I seed a Neon database with Prisma?#
Set directUrl in schema.prisma to Neon's unpooled connection string, keep url pointing at the pooled one for app queries, write your prisma/seed.ts, and run npx prisma db seed. If you deploy on an edge runtime, also install @prisma/adapter-neon, @neondatabase/serverless, and ws — but seeding itself usually runs in Node.js, not edge, so the adapter isn't required for the seed script.
How do I seed a Neon database with Drizzle?#
Use drizzle-orm/node-postgres with the pg driver and Neon's unpooled URL for the seed script. For serverless/edge app code, switch to drizzle-orm/neon-http (one-off queries) or drizzle-orm/neon-serverless (transactions over WebSocket). Seedfast can also seed Drizzle-managed schemas directly because it talks to Postgres rather than going through the ORM.
Why does my Neon seed work locally but fail in CI?#
The two most common causes: using the pooled URL in CI (switch to unpooled for seeds) and missing sslmode=require in the environment variable. Also check that migrations ran before the seed — CI often skips the migrate step when databases are recreated.
Related guides#
- How to seed a database: PostgreSQL practical guide — the framework-agnostic version of this article, covering raw SQL, Prisma, Drizzle, TypeORM, and
node-postgres - Database seeding: methods and best practices — the conceptual companion covering reference vs test data, idempotency, and when seed files stop scaling
- Seed file maintenance — why static seed files fall out of sync with your schema and what to do about it
- Microservice database seeding — when one Neon project isn't enough and you're seeding multiple databases that reference each other
- Get started with Seedfast — connect to your Neon database and run your first schema-aware seed