DEV Community

Cover image for How to Seed a Neon Database: psql, Prisma, Drizzle
Mikhail Shytsko
Mikhail Shytsko

Posted on • Originally published at seedfa.st on

How to Seed a Neon Database: psql, Prisma, Drizzle

To seed a Neon database, use the unpooled connection string with psql, Prisma, or Drizzle. The pooled URL breaks prepared statements mid-seed. Neon gives you an empty serverless Postgres in under a second; then you stare at it. This guide covers three ways to seed Neon database tables (psql, an ORM seed, and Seedfast, which reads your live Neon schema and generates FK-valid relational data from a plain-English scope with no seed scripts to maintain) plus how branching changes the seeding model.

Key Takeaways

  • If your seed throws prepared statement "s1" already exists or cached plan must not change result type, you're connecting through the pooled URL. Switch to the unpooled string (no -pooler in the hostname). PgBouncer transaction mode breaks prepared statements and times out long-running scripts
  • Neon's main branch starts empty. Every new project, every new branch, and every preview environment needs test data before the app is usable
  • Prisma and Drizzle both seed Neon fine, but Prisma on edge runtimes requires the @prisma/adapter-neon adapter plus ws; the standard pg driver 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, and it will, static seed files break. Seedfast notices the change on its next run and regenerates valid data, so there is no seed file to maintain

Quick fix if you landed here from a broken seed:

# Pooled URL — breaks seeding (PgBouncer transaction mode)
postgresql://user:pass@ep-xxxx-pooler.region.aws.neon.tech/dbname?sslmode=require

# Unpooled URL — use this for seeds, migrations, admin scripts
postgresql://user:pass@ep-xxxx.region.aws.neon.tech/dbname?sslmode=require

Enter fullscreen mode Exit fullscreen mode

You provisioned Neon because it spins up in milliseconds, 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.

We'll cover all three, starting with raw SQL, then Prisma and Drizzle seed scripts with Neon's serverless driver, then how Seedfast turns a plain-English scope into connected rows without a seed file to write. If you want the general PostgreSQL version first, how to seed a database covers the cross-framework fundamentals; this article is specifically about Neon.

When do you need to seed a Neon database?

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:

  1. New project onboarding. A developer clones the repo, creates a Neon project, and runs the migrations; the tables exist, but nothing else does.
  2. Preview branches per PR. The GitHub Action creates a branch for every pull request, and if the parent is empty, every preview is empty too, which means your end-to-end tests hit 404s.
  3. 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 branching docs push branching over seeding, arguing for forking from a parent that already has data so every child branch inherits it automatically. 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

Enter fullscreen mode Exit fullscreen mode

Note the -pooler in the pooled hostname. That's PgBouncer in transaction mode, which returns connections 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 COPY operations and large transactional seeds can hit statement timeouts
  • Some ORMs emit session-level SET statements 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

Enter fullscreen mode Exit fullscreen mode

Prisma calls this directUrl in schema.prisma, while Drizzle doesn't care and just wants 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, so switch to the unpooled URL.

Method 1: Seed Neon database with raw SQL

The simplest thing that works is writing INSERTs and running 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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

Raw SQL is fine for reference data like roles, feature flags, and country codes. Past ten tables with foreign keys, though, it starts breaking, 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

Enter fullscreen mode Exit fullscreen mode

If you deploy on Vercel Edge or Cloudflare Workers, the runtime has no TCP sockets. Install the Prisma Neon 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 });

Enter fullscreen mode Exit fullscreen mode

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, which suits serverless app code but not seed transactions, and drizzle-orm/neon-serverless for WebSocket sessions with full transaction support, the one 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

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

Either works, since the pg version is one less dependency while the serverless-driver version keeps your app and seed script on the same driver.

Both Prisma and Drizzle share the same ORM seed problem, since 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. Seedfast removes that break-on-migration step entirely, picking up the new column automatically and filling it without a seed-file edit.

Using @neondatabase/serverless with local Postgres for development

@neondatabase/serverless cannot talk to a regular local Postgres on localhost:5432. The HTTP driver (neon()) speaks HTTP to Neon's proxy, and the WebSocket driver (Pool from @neondatabase/serverless) speaks WebSocket to Neon's serverless gateway. Because plain Postgres only understands the binary wire protocol on TCP, the connection fails before any query runs.

This is the standard combination for apps deployed on Vercel Edge Functions or Cloudflare Workers against Neon, where production runs on the edge with the serverless driver while local development wants plain Postgres in Docker. There are two ways out. Swap the Drizzle driver per environment (recommended), or run a local WebSocket proxy that translates for @neondatabase/serverless.

Solution 1: swap drivers per environment (drizzle-orm/node-postgres locally, neon-http in production)

Keep drizzle-orm/neon-http (or drizzle-orm/neon-serverless) for production and use drizzle-orm/node-postgres against local Postgres. Drizzle's schema, types, and query API are identical across drivers, so only the file that constructs db changes.

// src/db/index.ts
import { drizzle as drizzleNeon } from "drizzle-orm/neon-http";
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
import { neon } from "@neondatabase/serverless";
import { Pool } from "pg";
import * as schema from "./schema";

const connectionString = process.env.DATABASE_URL!;

export const db =
  process.env.NODE_ENV === "production"
    ? drizzleNeon(neon(connectionString), { schema })
    : drizzlePg(new Pool({ connectionString }), { schema });

Enter fullscreen mode Exit fullscreen mode

Local .env:

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"

Enter fullscreen mode Exit fullscreen mode

Production .env:

DATABASE_URL="postgresql://user:pass@ep-xxxx.region.aws.neon.tech/dbname?sslmode=require"

Enter fullscreen mode Exit fullscreen mode

Your schema file and every query (db.select().from(users)…) stays the same. The only divergence is the driver factory. If you need transactions locally, node-postgres already supports them. neon-http does not, so any code that uses db.transaction(...) either has to live behind a server action that runs on Node, or you switch the production driver to drizzle-orm/neon-serverless (WebSocket, transactions supported on the edge).

It comes down to one process-env flip and two factories, and the query code stays identical either way.

Solution 2: local WebSocket proxy for @neondatabase/serverless

If you need a single code path that uses @neondatabase/serverless everywhere, run a local WebSocket-to-Postgres proxy. Neon's wsproxy accepts WebSocket connections on a local port and forwards them to a real Postgres instance, so the serverless driver thinks it's talking to Neon.

Minimal docker-compose.yml:

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: postgres
    ports: ["5432:5432"]

  wsproxy:
    image: ghcr.io/neondatabase/wsproxy:latest
    environment:
      APPEND_PORT: "postgres:5432"
      ALLOW_ADDR_REGEX: ".*"
    ports: ["4444:80"]
    depends_on: [postgres]

Enter fullscreen mode Exit fullscreen mode

Point @neondatabase/serverless at the local proxy:

import { Pool, neonConfig } from "@neondatabase/serverless";
import ws from "ws";

neonConfig.webSocketConstructor = ws;
neonConfig.wsProxy = (host) => `localhost:4444/v2`;
neonConfig.useSecureWebSocket = false;
neonConfig.pipelineTLS = false;
neonConfig.pipelineConnect = false;

const pool = new Pool({
  connectionString: "postgresql://postgres:postgres@localhost:5432/postgres",
});

Enter fullscreen mode Exit fullscreen mode

The trade-off is an extra container, an extra protocol to debug when something breaks, and a second handshake on every connection. Solution 1 needs one env check; Solution 2 needs a running container and a WebSocket connection to inspect. Reach for the proxy only if you need exact edge parity in local dev, for example hunting a bug that only reproduces over WebSocket.

drizzle-kit push local postgres "neon serverless" websocket warning

drizzle-kit push and drizzle-kit migrate always use node-postgres under the hood. The warning means your drizzle.config.ts is pointing at a Neon serverless or pooled URL, and drizzle-kit is telling you it will ignore the serverless bits and connect over plain TCP. Giving it a pg-compatible URL fixes it, Neon's unpooled direct connection for production and plain local Postgres for development.

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url:
      process.env.DATABASE_URL_UNPOOLED || "postgresql://postgres:postgres@localhost:5432/postgres",
  },
});

Enter fullscreen mode Exit fullscreen mode

For Neon, DATABASE_URL_UNPOOLED is the hostname without -pooler and with ?sslmode=require. Locally, plain postgresql://postgres:postgres@localhost:5432/postgres is enough. Do not try to silence the warning by setting driver: 'pg' plus a serverless URL. The underlying mismatch is the connection string, not the config key. (Drizzle-kit removed the driver field altogether in 0.21+, making the workaround a no-op on newer versions.)

drizzle-orm/neon-http vs drizzle-orm/neon-serverless vs drizzle-orm/node-postgres

Three drivers speak three different wire protocols behind one Drizzle query API.

Driver Wire protocol Transactions Runtime Works against
drizzle-orm/neon-http HTTPS to Neon proxy No Node + edge Neon only
drizzle-orm/neon-serverless WebSocket to Neon Yes Node + edge Neon only
drizzle-orm/node-postgres TCP wire protocol Yes Node only Any Postgres (including local)

Pick by deployment target:

  • Edge with one-shot queries, no transactions: use neon-http for the lowest latency on a single read.
  • Edge with transactions: use neon-serverless to keep a WebSocket session open, matching the edge-runtime story.
  • Local development, CI Postgres in Docker, or any non-Neon host: use node-postgres, since the serverless drivers won't connect here.

The conditional-import pattern from Solution 1 above lets you mix and match: node-postgres locally for pnpm dev, neon-http or neon-serverless for the deployed 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"

Enter fullscreen mode Exit fullscreen mode

When a migration adds a new table or column, the next seedfast seed picks it up automatically, with no file to update and no foreign key order to work out by hand.

In our internal runs, Seedfast generates around a million FK-valid rows into a typical 20-table SaaS schema in roughly three and a half minutes.

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"

Enter fullscreen mode Exit fullscreen mode

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, because that data belongs to the application, not a test dataset. Seedfast is built for development, CI, staging, and demo data; the handful of rows that actually ship to production still belongs in that versioned file.

Generated rows land straight in your Neon branch, with no CSV to import, no intermediate file to load, and no manual psql step at the end. Run the seed command once and the tables are full. See data handling and privacy for exactly what crosses the wire.

A free plan is available. Connect and run your first seed in about two minutes.

Generate test data for a Neon branch (that survives a branch reset)

After a branch reset, the thing that has to survive is the generator, not a git-tracked seed file. A schema-aware test data generator for Neon regenerates from the branch's current schema on every run, so repopulating means running the generator again instead of restoring a file. Neon lets you instantly reset a branch to its parent, which wipes every branch-local write. After a reset, you have to repopulate. With a static seed.sql that's a psql run; with a hand-edited ORM seed it's a psql run plus whatever migrations have landed since you last touched the file.

Two things make a Neon branch different from a plain database, and both favor reading the schema over replaying a file:

A branch created off main inherits main's schema at fork time, but if a migration lands on the branch in the same PR, the branch's tables no longer match the seed file written against main. Regenerating against the branch's own schema sidesteps that, since Seedfast targets the branch's real tables, columns, and FKs, not the ones from yesterday. And because a reset throws away branch-local data, the cheap loop becomes resetting the branch and rerunning the generator, instead of resetting the branch and then reviewing a diff on the seed file. Re-run the same scope against the branch's unpooled URL (the install and seedfast connect steps are in Method 3 above):

# After resetting the branch — repopulate from its current schema
seedfast seed --scope "3 users with completed onboarding, 5 draft posts, 2 published"

Enter fullscreen mode Exit fullscreen mode

Keeping neon branch seed data current becomes a property of the run instead of something stored in a file you maintain. Static psql/ORM seeds are still the right call for versioned reference rows like feature flags and country codes, because those belong in git and should survive a reset by being committed. For the deeper per-PR branching workflow (seed-parent-once, drift-reseed, schema-only rescue), see Neon branch seeding; for the broader tool decision across Postgres hosts, the best Postgres test data generator comparison weighs the options.

Seed Neon once, branch many times

Neon branching turns one seeded parent into many populated branches, since you seed the parent once and every branch forked from it inherits that dataset in milliseconds. Keeping that parent dataset current across schema changes is where Seedfast fits. Every run starts from the parent's schema as the migrations have left it, so parent data and schema never drift apart.

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"

Enter fullscreen mode Exit fullscreen mode

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 }}

Enter fullscreen mode Exit fullscreen mode

There's no seeding step in CI, because the branch already has data, inherited 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 }}

Enter fullscreen mode Exit fullscreen mode

The two pieces fit together when you seed the parent with a realistic dataset, then branch per PR for isolated preview environments. The only thing you have to get right is keeping the parent's seed data current, and that's exactly what Seedfast does, rebuilding the parent dataset each time from the schema the migrations have already produced.

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"

This is a server-side Postgres plan-cache error, where 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, and Neon rejects non-SSL connections outright. 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, per Neon's compute docs). 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; it never runs in 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
External dependency None — psql is everywhere None — already in your stack Yes — separate CLI plus a network call to your DB
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, which takes about two minutes and never asks for a credit card.

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, since 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, since it talks to Postgres instead of going through the ORM.

Can I use @neondatabase/serverless with local Postgres for development?

No. The @neondatabase/serverless package speaks HTTP (neon()) or WebSocket (Pool) to Neon's gateway, not the regular Postgres wire protocol. A plain Postgres at localhost:5432 will not accept either connection. Use drizzle-orm/node-postgres with the pg driver against local Postgres and keep drizzle-orm/neon-http or drizzle-orm/neon-serverless for production. If you need a single code path, run Neon's wsproxy in Docker so @neondatabase/serverless can reach local Postgres over WebSocket.

What connection string does @neondatabase/serverless use for local Postgres?

There is no working local connection string for @neondatabase/serverless unless you also run a WebSocket proxy in front of Postgres. For local development, switch to drizzle-orm/node-postgres and use postgresql://postgres:postgres@localhost:5432/postgres. Production keeps Neon's serverless string: postgresql://user:pass@ep-xxxx.region.aws.neon.tech/dbname?sslmode=require.

What's the best @neondatabase/serverless alternative driver for local Drizzle development?

drizzle-orm/node-postgres with the pg package. The Drizzle schema, types, and query code are identical to drizzle-orm/neon-http, so the only file that changes is the one constructing db. Pick the driver at runtime with process.env.NODE_ENV and your local app behaves like the deployed one. drizzle-orm/postgres-js (with postgres) works too and is slightly faster on cold connects, but node-postgres is the closest one-to-one swap.

Why does drizzle-kit push warn about neon serverless WebSocket against local Postgres?

drizzle-kit push and drizzle-kit migrate use node-postgres internally regardless of which Drizzle driver your app uses. If your drizzle.config.ts points at a Neon serverless or pooled URL, drizzle-kit prints a WebSocket warning and falls back to TCP. Give it a pg-compatible URL (Neon's unpooled direct connection or postgresql://postgres:postgres@localhost:5432/postgres for local) and the warning goes away. Do not switch the production app to pg just to silence drizzle-kit.

Why does my Neon seed work locally but fail in CI?

The two most common causes are using the pooled URL in CI (switch to unpooled for seeds) and a missing sslmode=require in the environment variable. Also check that migrations ran before the seed, since CI often skips the migrate step when databases are recreated.

What's the best test data generator for a Neon branch?

The best fit for a Neon branch survives a reset without a file to restore, regenerating the branch's tables with valid FKs in a single run no matter what migrations have landed since. Seedfast works this way against the branch's unpooled URL, and raw psql or ORM seeds remain the right choice for static reference data that should live in git and survive a reset by being committed.

How do I keep Neon branch seed data current when the schema changes?

Re-run a schema-aware generator after migrating the branch. It reads the new tables and columns and regenerates valid rows, so there's no file to hand-edit. A static seed.sql or ORM seed needs a manual update for every added column or foreign key. Really, it comes down to editing a file on every migration versus letting the generator re-read the schema, and for evolving test data, re-reading wins.

Related guides

Originally published at seedfa.st.

Top comments (0)