<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mikhail Shytsko</title>
    <description>The latest articles on DEV Community by Mikhail Shytsko (@mikh-shytsko).</description>
    <link>https://dev.to/mikh-shytsko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3948102%2Fa736e791-77e9-40e8-b6df-1059059f6f5e.jpg</url>
      <title>DEV Community: Mikhail Shytsko</title>
      <link>https://dev.to/mikh-shytsko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mikh-shytsko"/>
    <language>en</language>
    <item>
      <title>Synthetic Data for CI/CD: Seed Fresh, Valid Rows on Every Run</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:35:34 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/synthetic-data-for-cicd-seed-fresh-valid-rows-on-every-run-2fe8</link>
      <guid>https://dev.to/mikh-shytsko/synthetic-data-for-cicd-seed-fresh-valid-rows-on-every-run-2fe8</guid>
      <description>&lt;p&gt;Every CI pipeline that touches a database needs rows to run against, and there are only three ways to get them: restore a copy of production, replay a checked-in &lt;code&gt;seed.sql&lt;/code&gt;, or generate the data inside the run. Synthetic data for CI/CD is the third option — generate fresh, referentially-valid rows from the schema on every build, so no production data is copied and the data can't go stale between migrations the way a static seed file does. For most teams it's the one option that both stays correct as the schema changes and stays clean of real customer data.&lt;/p&gt;

&lt;p&gt;This guide covers why pipeline-time generation beats the other two, what a generator needs to be usable in CI, and the newer pressure behind it: AI coding agents that open pull requests and need a real database to test against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why generate synthetic data for CI/CD
&lt;/h2&gt;

&lt;p&gt;Generating synthetic data in the pipeline beats the two older approaches (copying production and replaying a static &lt;code&gt;seed.sql&lt;/code&gt;) on the two things CI cares about: it moves no real customer data, and it stays in sync with the schema. Each older approach fails one of those.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copying production&lt;/strong&gt; drags real customer data into a build environment. That's a compliance problem the moment the schema holds anything personal, and it usually means a masking pipeline bolted on top, plus a slow restore of a database far larger than any test needs. You inherit production's size and its risk to test a pull request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A checked-in &lt;code&gt;seed.sql&lt;/code&gt;&lt;/strong&gt; avoids the privacy problem but rots. The day someone adds a &lt;code&gt;NOT NULL&lt;/code&gt; column or a new foreign key, the file is wrong: it fails, or worse, quietly seeds an incomplete row. Static fixtures drift because the schema moves and the file doesn't. The &lt;a href="https://seedfa.st/blog/seed-file-maintenance" rel="noopener noreferrer"&gt;seed file maintenance&lt;/a&gt; problem is exactly this, and every migration charges the tax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating in the pipeline&lt;/strong&gt; sidesteps both. The data is &lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;synthetic&lt;/a&gt;, so there are no production records to move or mask, and because it's built from the live schema each run, a new migration is picked up automatically: the &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;pipeline-time seeding&lt;/a&gt; step re-reads the schema and writes rows that fit it. The catch is that the generator has to produce valid, connected data unattended, which is where most tools fall down. &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; is a schema-aware generator built for that: point it at a connection string and it reads the live schema itself.&lt;/p&gt;

&lt;p&gt;On the axes that decide whether a seed source survives in CI:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Moves production PII?&lt;/th&gt;
&lt;th&gt;Stays valid after a schema change?&lt;/th&gt;
&lt;th&gt;Sized for the test?&lt;/th&gt;
&lt;th&gt;Runs unattended in CI?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Copy / restore production&lt;/td&gt;
&lt;td&gt;Yes (real records)&lt;/td&gt;
&lt;td&gt;Only after a fresh copy and re-mask&lt;/td&gt;
&lt;td&gt;No (production-sized)&lt;/td&gt;
&lt;td&gt;Not without a masking step first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checked-in &lt;code&gt;seed.sql&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No, breaks when the schema moves&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Until the next migration breaks it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate in the pipeline&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, re-reads the live schema each run&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Only with a generator good enough to run on its own&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bottom row's last column is the honest cost of generating: it works only if the generator holds integrity without a human watching. &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Run your first seed&lt;/a&gt; free to put that on your own schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a CI seed step actually needs
&lt;/h2&gt;

&lt;p&gt;A CI seed step has to clear five bars a point-and-click generator never faces. Miss one and the step either won't run unattended or won't fail loudly enough to trust.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A non-interactive command.&lt;/strong&gt; It runs from a connection string and an API key in an environment variable, with no UI, no login prompt, and no paste step. If a human has to click, it isn't a CI step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A schema read at run time.&lt;/strong&gt; The seed runs &lt;em&gt;after&lt;/em&gt; migrations apply, so the generator must read the current schema then, not a snapshot from when someone configured it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Referential integrity across the &lt;a href="https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK" rel="noopener noreferrer"&gt;foreign-key graph&lt;/a&gt;.&lt;/strong&gt; Data that violates constraints fails the insert; data that skips relationships makes integration tests lie. The generator produces connected rows and handles &lt;a href="https://seedfa.st/blog/circular-foreign-key-seed" rel="noopener noreferrer"&gt;nullable circular foreign keys&lt;/a&gt; without hand-holding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machine-readable output and honest exit codes.&lt;/strong&gt; A JSON output mode and a non-zero exit on failure are how the build gate knows whether the seed worked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cost that doesn't scale with run frequency.&lt;/strong&gt; Seeds fire on every push, every branch, across a job matrix, and a per-row or per-token price turns that volume into a bill that climbs with your commit rate. A flat plan doesn't move.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test data for AI agents in the pipeline
&lt;/h2&gt;

&lt;p&gt;AI coding agents now open pull requests, and &lt;a href="https://seedfa.st/blog/agentic-qa-test-data" rel="noopener noreferrer"&gt;agentic test runners&lt;/a&gt; drive an app end to end on every PR. Neither one seeds the database it depends on. Both assume realistic, connected rows are already there: the agent that wrote the code and the agent that tests it each expect something else to have prepared the schema.&lt;/p&gt;

&lt;p&gt;That gap closes cleanly when the generator is both a CLI step and an &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; tool. Seedfast exposes the same seed run over the Model Context Protocol, so an agent in Claude Code or Cursor calls &lt;code&gt;seedfast_run&lt;/code&gt; to seed the branch database, then lets the tests (human-written or agent-driven) run against it. The seeded-data prerequisite collapses into one tool call. As more of the pipeline runs on agents, "the test data is generated, valid, and current" becomes the assumption everything else rests on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seedfast as the CI seed step
&lt;/h2&gt;

&lt;p&gt;Seedfast runs as a single step after migrations: one command points at the database, reads the live schema, and writes connected rows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/test.yml (excerpt)&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Seed test database&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx seedfast seed --scope "realistic accounts, orders, and line items" --output json&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;SEEDFAST_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEEDFAST_API_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;SEEDFAST_DSN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TEST_DATABASE_URL }}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--scope&lt;/code&gt; is plain English: Seedfast generates connected accounts, orders, and line items with valid foreign keys between them, sized to the test rather than to production. &lt;code&gt;--output json&lt;/code&gt; returns a machine-readable result, and the command exits non-zero on failure so the build gate can read it. Add a table or column in a later migration and the next run picks it up, with nothing to reconfigure. The &lt;code&gt;SEEDFAST_API_KEY&lt;/code&gt; comes from a free Seedfast account, and the &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD database seeding&lt;/a&gt; guide has the full &lt;a href="https://docs.github.com/en/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; and GitLab CI setup, including per-environment keys and exit-code handling — the &lt;a href="https://seedfa.st/blog/github-actions-seed-postgres-database" rel="noopener noreferrer"&gt;service container and health-check mechanics underneath that setup&lt;/a&gt; get their own walkthrough.&lt;/p&gt;

&lt;p&gt;For ephemeral databases this pairs with branch-per-PR workflows: the &lt;a href="https://seedfa.st/blog/neon-branching-seed-data" rel="noopener noreferrer"&gt;Neon branching seed data&lt;/a&gt; guide covers seeding a fresh branch database per run, and &lt;a href="https://seedfa.st/blog/e2e-test-fixtures" rel="noopener noreferrer"&gt;E2E test fixtures&lt;/a&gt; covers generating data for Playwright and Cypress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a generator for CI
&lt;/h2&gt;

&lt;p&gt;Not every test-data tool clears that bar. Web-based column generators like Mockaroo are built for clicking, so they don't drop into an unattended command. ORM-coupled seeders like Drizzle's and Prisma's &lt;code&gt;seed&lt;/code&gt; scripts need your application code running and only work inside one framework. Production-copy tools like pg_dump hand back the privacy and size problems you came here to avoid, and masking platforms like Tonic Structural still start from a copy of production to de-identify it — the production access CI-time synthetic data skips entirely. Even Faker-based scripts generate columns in isolation, with no way to keep the rows connected. Seedfast takes a connection string, reads the live schema, and holds referential integrity with nobody watching.&lt;/p&gt;

&lt;p&gt;If you're cross-shopping on that axis, the &lt;a href="https://seedfa.st/blog/best-postgres-test-data-generator" rel="noopener noreferrer"&gt;best Postgres test data generator&lt;/a&gt; comparison ranks tools on schema-awareness and CI fit, &lt;a href="https://seedfa.st/compare/seedfast-vs-tonic-fabricate" rel="noopener noreferrer"&gt;Seedfast vs Tonic Fabricate&lt;/a&gt; weighs a metered web agent against a flat CLI step, and the &lt;a href="https://seedfa.st/blog/data-seeding-tools" rel="noopener noreferrer"&gt;data seeding tools&lt;/a&gt; guide covers the regulated-industry angle where copying production isn't allowed at all. For the broader strategy of where test data comes from and how it stays valid, see &lt;a href="https://seedfa.st/blog/test-data-management" rel="noopener noreferrer"&gt;test data management&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is synthetic data for CI/CD?
&lt;/h3&gt;

&lt;p&gt;Synthetic data for CI/CD is test data generated inside the pipeline from your database schema, after migrations apply, rather than copied from production or replayed from a static seed file. Because it's regenerated from the live schema each run, it stays valid as the schema changes, and because it's synthetic it carries no real customer data.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is generating data in CI different from a copied production dump?
&lt;/h3&gt;

&lt;p&gt;A copied dump is production-sized, so a multi-gigabyte restore can take minutes on every job, and it usually needs a masking step before it's safe to use. A generated seed is sized to the test and skips masking entirely, because there's no real data in it to mask. The dump also reflects whatever the schema looked like when it was taken; the generator re-reads the current schema instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a synthetic data generator keep referential integrity in CI?
&lt;/h3&gt;

&lt;p&gt;It has to, or the seed is useless. A generator built for CI has to produce parent and child rows that reference each other correctly, including tables with circular references. Seedfast handles this automatically, so the data that lands is referentially valid with no manual ordering on your side.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does seeding in CI send my data to an AI provider?
&lt;/h3&gt;

&lt;p&gt;No rows leave your database. The generator reads the schema's shape (table and column names, types, and constraints) to plan the data, then generates the values itself; your production records are never involved, because they aren't in the test database to begin with. If a table or column name is itself sensitive, confirm that path fits your policy before wiring it into CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does seeding on every pipeline run get expensive?
&lt;/h3&gt;

&lt;p&gt;It depends on the pricing model. A four-job matrix on a twenty-push day is eighty seed runs; a per-row or per-token generator bills all eighty, scaling with your commit rate. Seedfast sells a flat monthly plan that includes a pool of credits, and a run draws from that pool by how much data it generates, with no per-row or per-token meter underneath. Eighty runs on a mid-size schema is roughly $48 of credits, well inside Premium's $180, and the monthly bill itself doesn't move with your commit rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seed your CI database as a pipeline step
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; connects to your PostgreSQL database, reads the live schema, and generates connected, realistic data through a single CLI step or an MCP tool, fresh and referentially-valid on every pipeline run, with no copied production data and no &lt;code&gt;seed.sql&lt;/code&gt; to patch. &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;Set up CI/CD seeding&lt;/a&gt; or &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;run your first seed&lt;/a&gt; in about two minutes — the free plan doesn't ask for a card. See &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; for the paid tiers.&lt;/p&gt;

&lt;p&gt;Related guides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD Database Seeding&lt;/a&gt;: the full GitHub Actions and GitLab CI setup&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;Staging Without Production Data&lt;/a&gt;: generate instead of copy, for lower environments&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/neon-branching-seed-data" rel="noopener noreferrer"&gt;Neon Branching Seed Data&lt;/a&gt;: seed a fresh branch database per run&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/e2e-test-fixtures" rel="noopener noreferrer"&gt;E2E Test Fixtures&lt;/a&gt;: generate data for Playwright and Cypress runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/synthetic-data-ci-cd" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>githubactions</category>
      <category>postgres</category>
      <category>testing</category>
    </item>
    <item>
      <title>ALTER TABLE, 5 Million Rows, and the Deploy That Took Down the Site</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:34:58 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/alter-table-5-million-rows-and-the-deploy-that-took-down-the-site-1fk5</link>
      <guid>https://dev.to/mikh-shytsko/alter-table-5-million-rows-and-the-deploy-that-took-down-the-site-1fk5</guid>
      <description>&lt;p&gt;&lt;em&gt;A migration that takes 50ms on your dev database can lock a production table for 20 minutes, and running it against production-scale volume first is how you catch that before your users do.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Thursday-afternoon deploy looked harmless. Adding a &lt;code&gt;NOT NULL&lt;/code&gt; column with a default value to the &lt;code&gt;orders&lt;/code&gt; table had passed review and run fine on staging, and the pipeline showed green.&lt;/p&gt;

&lt;p&gt;Then production went quiet, the way an on-call channel goes quiet, because the &lt;code&gt;orders&lt;/code&gt; table, all 8 million rows of it, sat locked while every API endpoint that touched orders queued and the load balancers began returning 502s. Forty minutes passed before the locks released and the retro could start. The schema change was exactly what the team needed, and the outage came down to a single missing step — nobody had run it against 8 million rows anywhere but production.&lt;/p&gt;

&lt;p&gt;This article covers migration &lt;em&gt;performance&lt;/em&gt; — lock duration, rewrite time, and the row counts that turn a 50ms change into a 40-minute outage. Its correctness companion, catching NULL concatenations and failed constraints before they ship, is &lt;a href="https://seedfa.st/blog/migration-review" rel="noopener noreferrer"&gt;how to review a migration against realistic data&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Migration time scales with data volume.&lt;/strong&gt; A change that runs in 50ms on 50 development rows can hold a lock for minutes on millions of production rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dangerous operations rewrite or scan the whole table&lt;/strong&gt; — backfilling a new column, a non-concurrent &lt;code&gt;CREATE INDEX&lt;/code&gt;, an &lt;code&gt;integer&lt;/code&gt;-to-&lt;code&gt;bigint&lt;/code&gt; type change, and validating a new &lt;code&gt;FOREIGN KEY&lt;/code&gt;. Each one holds a lock proportional to row count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some changes are metadata-only and safe at any size&lt;/strong&gt; — adding a column with a constant default (PostgreSQL 11+), widening a &lt;code&gt;varchar&lt;/code&gt;, or &lt;code&gt;varchar&lt;/code&gt; to &lt;code&gt;text&lt;/code&gt;. Reading the SQL won't tell you which class you're in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock duration, not total runtime, is what takes the site down.&lt;/strong&gt; A two-minute migration that never holds a lock longer than 200ms is safer than a 30-second one that locks the table the entire time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can time any migration before it ships by seeding production-scale, foreign-key-valid data with &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt;&lt;/strong&gt; and watching for lock contention as it runs. The data-shape side of the same check, at small volume, lives in &lt;a href="https://seedfa.st/blog/migration-review" rel="noopener noreferrer"&gt;migration review&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Migration Time Bomb
&lt;/h2&gt;

&lt;p&gt;Schema migrations carry a property most code changes lack. Their execution time scales with data volume, because on many operations PostgreSQL rewrites or scans every row rather than only updating metadata.&lt;/p&gt;

&lt;p&gt;This creates a class of problems that are completely invisible in development:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;100 rows&lt;/th&gt;
&lt;th&gt;1M rows&lt;/th&gt;
&lt;th&gt;5M rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ADD COLUMN ... DEFAULT&lt;/code&gt; (pre-PG 11)&lt;/td&gt;
&lt;td&gt;&amp;lt; 1ms&lt;/td&gt;
&lt;td&gt;8s&lt;/td&gt;
&lt;td&gt;42s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ALTER COLUMN TYPE&lt;/code&gt; (integer to bigint)&lt;/td&gt;
&lt;td&gt;&amp;lt; 1ms&lt;/td&gt;
&lt;td&gt;12s&lt;/td&gt;
&lt;td&gt;65s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CREATE INDEX&lt;/code&gt; (single column)&lt;/td&gt;
&lt;td&gt;&amp;lt; 1ms&lt;/td&gt;
&lt;td&gt;4s&lt;/td&gt;
&lt;td&gt;22s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ADD COLUMN NOT NULL DEFAULT&lt;/code&gt; (PG 11+)&lt;/td&gt;
&lt;td&gt;&amp;lt; 1ms&lt;/td&gt;
&lt;td&gt;&amp;lt; 1ms&lt;/td&gt;
&lt;td&gt;&amp;lt; 1ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt; 1ms&lt;/td&gt;
&lt;td&gt;6s&lt;/td&gt;
&lt;td&gt;35s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Figures are illustrative orders of magnitude on commodity hardware; real numbers depend on your hardware, table width, indexes, and concurrent load.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That fourth row is worth a look. &lt;a href="https://www.postgresql.org/docs/current/ddl-alter.html#DDL-ALTER-ADDING-A-COLUMN" rel="noopener noreferrer"&gt;PostgreSQL 11 made &lt;code&gt;ADD COLUMN ... DEFAULT&lt;/code&gt; a metadata-only operation&lt;/a&gt; for constant defaults, while the other operations still rewrite or scan the table and even the fast ones have nuances that bite at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  5 Migrations That Look Harmless at 100 Rows
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Adding a NOT NULL Column With Backfill
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1: Looks fine&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2: The backfill that locks the table&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'us-east-1'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3: Now add the constraint&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backfill in step 2 is where it goes wrong. A single &lt;code&gt;UPDATE&lt;/code&gt; touching 5 million rows acquires row locks across the entire table, so concurrent writes block, and autovacuum can't reclaim the dead tuples the backfill creates while the transaction stays open. If you have foreign key references, those tables might lock too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At scale&lt;/strong&gt; , a 5M-row backfill &lt;code&gt;UPDATE&lt;/code&gt; can take 30+ seconds and block all concurrent writes to the table for the duration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The safer pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Batch the backfill&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'us-east-1'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Repeat until no rows remain&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Creating an Index on a Large Table
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Acquires a SHARE lock — blocks writes for the entire build&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 100 rows it finishes instantly, but on 10 million rows it holds a &lt;code&gt;SHARE&lt;/code&gt; lock for the entire index build, so every &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; on that table queues behind it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The safer pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Builds the index without blocking writes&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY" rel="noopener noreferrer"&gt;&lt;code&gt;CONCURRENTLY&lt;/code&gt;&lt;/a&gt; doesn't block writes, but it takes roughly 2-3x longer and can fail if there are concurrent schema changes. It also can't run inside a transaction block, which means most migration frameworks need special handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Changing a Column Type
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Widening an integer primary key that's about to overflow&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every growing team eventually runs this one, an &lt;code&gt;integer&lt;/code&gt; ID column approaching its ~2.1 billion ceiling. It reads like a one-line metadata tweak, but because &lt;code&gt;integer&lt;/code&gt; and &lt;code&gt;bigint&lt;/code&gt; have different on-disk representations, PostgreSQL has to &lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES" rel="noopener noreferrer"&gt;rewrite the entire table&lt;/a&gt; while holding an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock, so a 5M-row table stays fully locked until it finishes.&lt;/p&gt;

&lt;p&gt;The catch is that not every type change rewrites. Widening a &lt;code&gt;varchar&lt;/code&gt;, or going from &lt;code&gt;varchar&lt;/code&gt; to &lt;code&gt;text&lt;/code&gt;, is binary-coercible, so PostgreSQL changes only the catalog and returns instantly even on 50 million rows, while &lt;code&gt;id INTEGER&lt;/code&gt; to &lt;code&gt;id BIGINT&lt;/code&gt; rewrites the whole table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The safer pattern for a true rewrite&lt;/strong&gt; adds a new column, backfills in batches, swaps with a rename, and drops the old column, which is more steps but never holds an extended lock.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Adding a Foreign Key Constraint
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_orders_customer&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a foreign key makes PostgreSQL validate every existing row, a full table scan over 5 million orders while it holds a lock on both &lt;code&gt;orders&lt;/code&gt; and &lt;code&gt;customers&lt;/code&gt;. If &lt;code&gt;customers&lt;/code&gt; is also large, you've locked two critical tables at once, and it only works if every order already points at a real customer, the kind of &lt;a href="https://seedfa.st/blog/referential-integrity" rel="noopener noreferrer"&gt;referential integrity&lt;/a&gt; that production data drifts away from over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The safer pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Add the constraint without validating existing rows&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_orders_customer&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;VALID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Validate in a separate step (holds a weaker lock)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;VALIDATE&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_orders_customer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html" rel="noopener noreferrer"&gt;&lt;code&gt;NOT VALID&lt;/code&gt;&lt;/a&gt; adds the constraint for new rows immediately, and then &lt;code&gt;VALIDATE&lt;/code&gt; checks the existing rows under a less aggressive lock whose duration still tracks the row count.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Dropping a Column (Yes, Really)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;legacy_status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In PostgreSQL, &lt;code&gt;DROP COLUMN&lt;/code&gt; doesn't rewrite the table; it only marks the column as invisible. That sounds fast, and usually it is, but the operation still takes an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock, so if long-running queries are reading from the table, the &lt;code&gt;DROP&lt;/code&gt; waits for them to finish, and while it waits, every new query queues behind it. A 1ms metadata operation can block the table for minutes when a slow &lt;code&gt;SELECT&lt;/code&gt; is running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At scale&lt;/strong&gt; , the lock acquisition time becomes unpredictable, driven by whatever concurrent query workload happens to be running when the &lt;code&gt;DROP&lt;/code&gt; fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Seedfast Workflow: Seed, Migrate, Measure
&lt;/h2&gt;

&lt;p&gt;The fix is to run your migration against production-scale data first. Seedfast generates realistic, relational data straight from your schema, so you can stand up a table at production row counts, without copying production rows or maintaining seed scripts, and time the migration against it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Seed Production-Scale Data
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Match your production table sizes&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 5 million orders with customers, order items, and payments"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated rows come out FK-valid and realistically distributed, with proportions that match the scope you asked for (batching and scope sizing for the very largest runs are covered in the &lt;a href="https://seedfa.st/docs/large-volume-seeding" rel="noopener noreferrer"&gt;large-volume seeding guide&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seeding Plan:
  public.customers — 500,000 records
  public.orders — 5,000,000 records
  public.order_items — 12,000,000 records
  public.payments — 4,800,000 records

Total: 22,300,000 records across 4 tables

Approve? (Y/n)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Run the Migration and Measure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Time the migration&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;psql &lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; migrations/20260225_add_region_column.sql

&lt;span class="c"&gt;# Or with your migration framework&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;flyway migrate
&lt;span class="nb"&gt;time &lt;/span&gt;rails db:migrate
&lt;span class="nb"&gt;time &lt;/span&gt;alembic upgrade &lt;span class="nb"&gt;head&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Check for Lock Contention
&lt;/h3&gt;

&lt;p&gt;While the migration runs, open another terminal and monitor locks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- See which queries are blocked and what's blocking them&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blocked_pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blocked_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;blocking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blocking_pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;blocking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blocking_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_start&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;waiting_time&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blocked&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blocking&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;blocking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_blocking_pids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Queries stacking up behind your migration are exactly what production will do, only with real user traffic behind them; PostgreSQL's &lt;a href="https://www.postgresql.org/docs/current/explicit-locking.html" rel="noopener noreferrer"&gt;explicit locking documentation&lt;/a&gt; explains which lock modes conflict.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Benchmark the Safe Alternative
&lt;/h3&gt;

&lt;p&gt;If the naive migration locks the table for too long, implement the safer pattern and measure again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Reseed a fresh database (or use a separate test database)&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 5 million orders with customers"&lt;/span&gt;

&lt;span class="c"&gt;# Run the batched migration&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;psql &lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; migrations/20260225_add_region_column_safe.sql

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have concrete numbers your team can decide on: "The naive migration locks &lt;code&gt;orders&lt;/code&gt; for 38 seconds. The batched version takes 2 minutes total but never holds a lock for more than 200ms."&lt;/p&gt;

&lt;p&gt;Run both candidate migrations against the same populated copy and you can see which lock pattern actually holds up (&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;the seeding guide&lt;/a&gt; covers a first run).&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD: Automated Migration Benchmarking
&lt;/h2&gt;

&lt;p&gt;Wire this into your pipeline so it stops being a manual exercise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Migration Benchmark&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;migrations/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;benchmark-migration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bench&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bench&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bench&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5432:5432&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Apply base schema&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;for f in migrations/*.sql; do&lt;/span&gt;
            &lt;span class="s"&gt;psql $DATABASE_URL -f "$f" 2&amp;gt;/dev/null || true&lt;/span&gt;
          &lt;span class="s"&gt;done&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://bench:bench@localhost:5432/bench&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Seed test data&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seedfast seed --scope "seed 1 million orders with customers and payments" --output plain&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SEEDFAST_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEEDFAST_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://bench:bench@localhost:5432/bench&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Benchmark new migration&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;NEW_MIGRATIONS=$(git diff --name-only origin/main -- migrations/ | sort)&lt;/span&gt;
          &lt;span class="s"&gt;for f in $NEW_MIGRATIONS; do&lt;/span&gt;
            &lt;span class="s"&gt;echo "--- Benchmarking: $f ---"&lt;/span&gt;
            &lt;span class="s"&gt;time psql $DATABASE_URL -f "$f"&lt;/span&gt;
          &lt;span class="s"&gt;done&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://bench:bench@localhost:5432/bench&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check migration duration&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "Review migration timing above. Migrations over 30s need batching or CONCURRENTLY."&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every PR with a migration gets benchmarked against realistic data, so a 200-row staging pass no longer vouches for a change. For running it unattended, the &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD database seeding guide&lt;/a&gt; covers non-interactive mode and spinning up a database per PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  For Smaller Teams
&lt;/h2&gt;

&lt;p&gt;Even without full CI integration, you can add a one-liner to your PR template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Migration Checklist&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [] Tested against production-scale data (&lt;span class="sb"&gt;`seedfast seed --scope "..."`&lt;/span&gt;)
&lt;span class="p"&gt;-&lt;/span&gt; [] Migration completes in under 30 seconds on 1M+ rows
&lt;span class="p"&gt;-&lt;/span&gt; [] No ACCESS EXCLUSIVE locks held for more than 5 seconds
&lt;span class="p"&gt;-&lt;/span&gt; [] Uses CONCURRENTLY for index creation (if applicable)
&lt;span class="p"&gt;-&lt;/span&gt; [] Backfills are batched (if applicable)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  PostgreSQL Migration Gotchas Worth Knowing
&lt;/h2&gt;

&lt;p&gt;A slow migration is just one of &lt;a href="https://seedfa.st/blog/small-data-big-lies" rel="noopener noreferrer"&gt;the bugs that only real test data catches&lt;/a&gt;; the same volume blind spot behind slow queries and N+1s hides slow migrations too. A few more that trip teams up at scale:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transaction-wrapped migrations lock longer than you think.&lt;/strong&gt; Most frameworks run each migration inside a transaction, which means the lock acquired at the start isn't released until the whole file finishes, backfills and constraint validations and index builds included.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;CONCURRENTLY&lt;/code&gt; can't run in a transaction, a framework that wraps migrations by default (Rails, Flyway, Alembic) makes &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; fail until you configure that specific migration to run outside the transaction block.&lt;/p&gt;

&lt;p&gt;For zero-downtime rewrites like a column-type change, &lt;code&gt;pg_repack&lt;/code&gt; rebuilds a table in the background with minimal locking, though on a 50M-row table it still takes significant time and I/O, so test it against real volume first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Autovacuum can fall behind after a large backfill.&lt;/strong&gt; The dead tuples an &lt;code&gt;UPDATE&lt;/code&gt; leaves behind cause table bloat that slows subsequent queries if autovacuum can't keep up, so test the migration and then check bloat with &lt;code&gt;pg_stat_user_tables&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Run That Migration in Production
&lt;/h2&gt;

&lt;p&gt;Checklist for any migration touching a table with more than 100K rows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seed production-scale data locally&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed [your production row count] [table] with related records"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time the migration.&lt;/strong&gt; If it takes more than 10 seconds, consider batching or alternative approaches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor locks during execution.&lt;/strong&gt; An &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock held for more than a few seconds will impact production traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the rollback too.&lt;/strong&gt; A 30-second migration with a 5-minute rollback is a risky deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it during low traffic&lt;/strong&gt; if the lock duration is unavoidable, and know exactly how long that is, in seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check &lt;code&gt;CONCURRENTLY&lt;/code&gt; support&lt;/strong&gt; in your migration framework for index operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate constraints separately&lt;/strong&gt; using &lt;code&gt;NOT VALID&lt;/code&gt; + &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt; for foreign keys and check constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch backfills&lt;/strong&gt; — never &lt;code&gt;UPDATE&lt;/code&gt; millions of rows in a single statement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can't measure lock duration on 50 rows, benchmark a backfill on an empty table, or catch &lt;code&gt;CONCURRENTLY&lt;/code&gt; failing inside your framework's transaction wrapper without production-scale data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do you test a database migration before running it in production?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;You test a migration by running it against a database seeded with production-scale data and measuring how long it locks each table.&lt;/strong&gt; Stand up a database on the current schema, seed it to the same row counts as production, run the migration with a timer, and monitor &lt;code&gt;pg_locks&lt;/code&gt; in a second session while it executes. Whatever lock duration shows up there is what production will hit, only with live traffic queuing behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does a migration that runs fast in development lock the table in production?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Because most migration cost scales with row count, and a development database has almost no rows.&lt;/strong&gt; A backfill &lt;code&gt;UPDATE&lt;/code&gt;, a non-concurrent &lt;code&gt;CREATE INDEX&lt;/code&gt;, or an &lt;code&gt;integer&lt;/code&gt;-to-&lt;code&gt;bigint&lt;/code&gt; rewrite touches every row, so the same statement that finishes in under a millisecond on 50 dev rows can hold a lock for 40 seconds on 8 million production rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which PostgreSQL operations require a full table rewrite?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Changing a column to an incompatible type (such as &lt;code&gt;integer&lt;/code&gt; to &lt;code&gt;bigint&lt;/code&gt;), adding a column with a volatile default, and some &lt;code&gt;SET&lt;/code&gt;/&lt;code&gt;DROP&lt;/code&gt; operations rewrite the whole table under an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock.&lt;/strong&gt; Operations that are binary-coercible do not rewrite: widening a &lt;code&gt;varchar&lt;/code&gt;, &lt;code&gt;varchar&lt;/code&gt; to &lt;code&gt;text&lt;/code&gt;, and, since PostgreSQL 11, adding a column with a constant default are metadata-only and instant at any size.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you add an index to a large Postgres table without downtime?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt;, which builds the index without blocking writes.&lt;/strong&gt; It takes roughly 2–3× longer than a plain &lt;code&gt;CREATE INDEX&lt;/code&gt; and cannot run inside a transaction block, so most migration frameworks (Rails, Flyway, Alembic) need that specific migration configured to run outside their default transaction wrapper. A plain &lt;code&gt;CREATE INDEX&lt;/code&gt; holds a &lt;code&gt;SHARE&lt;/code&gt; lock that queues every write for the entire build.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use a copy of production data to test migration performance?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A production copy answers the volume question, but it drags real customer records (and their compliance exposure) into an environment with weaker controls.&lt;/strong&gt; By the time you refresh it, the schema has usually moved as well. Seeding the same row counts from the schema itself gives you identical lock behavior with nothing sensitive in the database. Seedfast works from your schema definitions rather than your rows, passing them to an external AI service to generate the data, so the thing to check is whether that schema handling fits your data-governance rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does Seedfast help test migrations at scale?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;It gives you a database at production row counts to benchmark against, seeded with FK-valid rows generated from your live schema.&lt;/strong&gt; You point it at a local or throwaway CI database, where it reads the schema structure without touching your production rows. There's a free plan that doesn't ask for a card, and because the schema is read fresh on every run, the same command keeps working after a migration changes the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/migration-review" rel="noopener noreferrer"&gt;Review SQL Migrations in 30 Seconds&lt;/a&gt; — the data-shape half of the same check: catching NULLs, duplicates, and silent truncation at small volume during code review&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/load-testing-data" rel="noopener noreferrer"&gt;Load Testing Data: Why Your Benchmarks Lie&lt;/a&gt; — the other half of production-scale testing, for query latency and throughput rather than migration locks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/test-data-postgresql" rel="noopener noreferrer"&gt;PostgreSQL Test Data: A Syntax Cookbook&lt;/a&gt; — the raw SQL patterns for generating volume and realistic distributions by hand&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/small-data-big-lies" rel="noopener noreferrer"&gt;Small Data, Big Lies: 6 Bugs Only Real Test Data Catches&lt;/a&gt; — why a 50-row development database hides the failures that only appear at scale&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/circular-foreign-key-seed" rel="noopener noreferrer"&gt;Circular Foreign Key Seed&lt;/a&gt; — handling FK cycles when you seed the large, related datasets a migration test needs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;Your Staging Database Is a Compliance Violation Waiting to Happen&lt;/a&gt; — generating realistic test data instead of copying and anonymizing production&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Get started with Seedfast&lt;/a&gt; — connect your database and seed production-scale data for your next migration test&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test the migration at production scale before you deploy it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Get Started&lt;/a&gt; | &lt;a href="https://seedfa.st/docs" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; | &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Seed a test database to production row counts with Seedfast and time your next migration against volume that behaves like the real thing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/migration-testing" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>devops</category>
      <category>sql</category>
      <category>database</category>
    </item>
    <item>
      <title>E2E Tests Without Brittle Fixtures: Generate Data on the Fly</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:34:22 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/e2e-tests-without-brittle-fixtures-generate-data-on-the-fly-3fin</link>
      <guid>https://dev.to/mikh-shytsko/e2e-tests-without-brittle-fixtures-generate-data-on-the-fly-3fin</guid>
      <description>&lt;p&gt;&lt;em&gt;Flaky Playwright and Cypress runs trace back, more often than anyone expects, to e2e test fixtures that quietly stopped matching the schema after last Tuesday’s migration.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You have probably watched a Cypress test that passed for months fail on a Monday, though nobody touched the test or the feature. The cause is almost always the data — a column landed on &lt;code&gt;users&lt;/code&gt; over the weekend while &lt;code&gt;cypress/fixtures/users.json&lt;/code&gt; kept the old shape, the shared test database got wiped, or the hardcoded user &lt;code&gt;42&lt;/code&gt; an assertion needs was deleted by a parallel test.&lt;/p&gt;

&lt;p&gt;Failures like these stay quiet, so the cost is easy to miss. It surfaces as one flaky test this week and two the next, until someone loses half a day “fixing” tests that were never wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema drift is the hidden cause behind most fixture failures.&lt;/strong&gt; Every migration behind a checked-in fixture adds maintenance nobody scheduled, and the failure surfaces in the test runner while the &lt;a href="https://seedfa.st/blog/small-data-big-lies" rel="noopener noreferrer"&gt;small-data bugs that only real data catches&lt;/a&gt; stay hidden underneath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The five anti-patterns share one habit, treating test data as shared, mutable state.&lt;/strong&gt; Shared databases, hardcoded IDs, checked-in fixtures, test-order dependencies, and restored dumps each block isolation, and without it you can’t run tests in parallel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the live schema generate the data your tests need.&lt;/strong&gt; A migration that adds a column is picked up on the next run, with no fixture-update PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query for the shape of the data a test needs rather than a hardcoded ID.&lt;/strong&gt; Asking for a user who has orders is a mechanical rewrite, and it’s what makes generated data usable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; turns the schema itself into the test-data source.&lt;/strong&gt; Wired in as a setup step, it seeds a fresh database on every E2E run from the live structure (tables, constraints, foreign keys), so any local or CI database works as a target.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Fixture Maintenance Trap
&lt;/h2&gt;

&lt;p&gt;The first fixture you write always looks completely harmless. You need a single user to exercise the login flow, so you create &lt;code&gt;test-user.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"testuser@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Test User"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the &lt;code&gt;users&lt;/code&gt; table gets a &lt;code&gt;department_id&lt;/code&gt; foreign key the fixture knows nothing about. Its seeded row fails to insert, or comes in with a &lt;code&gt;NULL&lt;/code&gt; the code never expected, and the E2E test blows up with a cryptic error far from the real cause.&lt;/p&gt;

&lt;p&gt;So someone adds &lt;code&gt;department_id&lt;/code&gt;. But a department has to exist first, so &lt;code&gt;departments.json&lt;/code&gt; appears, and the pull repeats for every related table, until six months later you have 30 fixture files in a dependency graph nobody fully understands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cypress/fixtures/
  departments.json
  users.json # depends on departments
  categories.json
  products.json # depends on categories
  orders.json # depends on users
  order_items.json # depends on orders AND products
  payments.json # depends on orders
  shipping.json # depends on orders AND users
  reviews.json # depends on users AND products

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every schema migration is now a fixture migration too. As the ORM models and API contracts change, a JSON file somewhere drifts out of sync, and the tests fail only later, on a fresh database, in a new CI container, or when stale data meets new code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five Anti-Patterns That Make It Worse
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Shared Test Database
&lt;/h3&gt;

&lt;p&gt;The whole QA team points at one database. Tests pass locally because the data happens to be there, CI fails because someone truncated the table, and two developers running it at once step on each other’s rows, none of it reproducible when the state differs per machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Developer&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice@test.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Developer&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bob@test.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: duplicate key value violates unique constraint "users_pkey"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The usual fix, random suffixes, only pushes the mess into the assertions.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Hardcoded IDs and Magic Values
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cypress test&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users/42/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-testid="order-row"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;have.length&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test assumes user 42 exists and has exactly 3 orders, so it breaks the moment the seed data shifts, somebody cleans the database, or CI starts auto-increment from a different number. The failure reads “expected 3, got 0” and explains nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fixture Files Checked Into Git
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fixtures/orders.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;updated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;months&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ago&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;99.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;149.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;29.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four months ago the &lt;code&gt;orders&lt;/code&gt; table had no &lt;code&gt;currency&lt;/code&gt; column, no &lt;code&gt;shipping_address_id&lt;/code&gt; foreign key, and no &lt;code&gt;NOT NULL&lt;/code&gt; on &lt;code&gt;created_at&lt;/code&gt;, and the fixture still doesn’t know. It inserts silently with defaults until one is missing or a constraint rejects the row, leaving the code to assume a field never provided.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Test-Order Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Order flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;creates a user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Creates user, stores ID in Cypress alias&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;creates an order for the user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Depends on the previous test having run successfully&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verifies the order appears in the list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Depends on BOTH previous tests&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-testid="order-row"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If “creates a user” fails or is skipped, every later test fails too. Nothing runs in parallel or in isolation, because the suite is a chain and one broken link takes down everything after it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The “Just Restore a Dump” Approach
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# CI pipeline&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restore test database&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg_restore --clean --no-owner -d testdb fixtures/test_dump.sql&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Database dumps are the heavyweight version of fixture files, large and usually binary, painful to diff, and just as quick to drift. They carry a developer’s machine assumptions about sequences and PostgreSQL versions, and a dump from production inherits every privacy and compliance liability of &lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;running real production data through staging&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Test Isolation Problem
&lt;/h2&gt;

&lt;p&gt;The same mistake sits under all five — data any test can read, write, or delete without the others knowing.&lt;/p&gt;

&lt;p&gt;Share data and isolation goes first, taking parallel execution with it, and an 8-minute E2E suite stretches to 45. Developers then stop running it locally, so bugs a local run would have caught slip into CI, and fixes that took minutes now take hours.&lt;/p&gt;

&lt;p&gt;The dependency chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shared fixtures
  -&amp;gt; Tests depend on each other's data
    -&amp;gt; No parallel execution
      -&amp;gt; Slow test suite
        -&amp;gt; Developers skip it
          -&amp;gt; Bugs in CI
            -&amp;gt; Slow feedback loops

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every serious testing framework recommends test isolation as a core principle — &lt;a href="https://playwright.dev/docs/browser-contexts" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; gives each test its own browser context, and &lt;a href="https://docs.cypress.io/app/core-concepts/test-isolation" rel="noopener noreferrer"&gt;Cypress&lt;/a&gt; resets browser state before every test. But isolation means each test must set up its own data, and if that setup is a fixture file, you’re back where you started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate Data, Don’t Maintain It
&lt;/h2&gt;

&lt;p&gt;The alternative is to generate fresh data from the schema before each suite run, skipping the fixture file and the dump entirely.&lt;/p&gt;

&lt;p&gt;The workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with an empty database (or &lt;a href="https://www.postgresql.org/docs/current/sql-truncate.html" rel="noopener noreferrer"&gt;&lt;code&gt;TRUNCATE&lt;/code&gt;&lt;/a&gt; the tables)&lt;/li&gt;
&lt;li&gt;Generate the data your tests need&lt;/li&gt;
&lt;li&gt;Run the tests&lt;/li&gt;
&lt;li&gt;Tear down&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each run gets fresh data matching the current schema, so nothing drifts and a new column shows up automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seedfast as a Test Setup Step
&lt;/h2&gt;

&lt;p&gt;Seedfast slots in front of your E2E suite as a setup step. Point the CLI at the live database and it works out the schema, then writes realistic, relational data to match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before your E2E suite runs:&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"5 users with 3 orders each, all with payments and shipping addresses"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;NOT NULL&lt;/code&gt; &lt;code&gt;currency&lt;/code&gt; column next sprint, and the next run simply includes a value for it; the fixture file that would have needed editing no longer exists.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://seedfa.st/docs/scoping" rel="noopener noreferrer"&gt;&lt;code&gt;--scope&lt;/code&gt; flag&lt;/a&gt; describes what you need in plain language, and Seedfast works out how everything connects. You never sequence tables by hand, and the generated rows hold the same &lt;a href="https://seedfa.st/blog/referential-integrity" rel="noopener noreferrer"&gt;referential integrity&lt;/a&gt; your fixtures kept breaking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Example: E-Commerce Test Suite
&lt;/h2&gt;

&lt;p&gt;Say you have a Playwright test suite for an e-commerce application. The tests cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User registration and login&lt;/li&gt;
&lt;li&gt;Product browsing and search&lt;/li&gt;
&lt;li&gt;Cart operations&lt;/li&gt;
&lt;li&gt;Checkout flow&lt;/li&gt;
&lt;li&gt;Order history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each needs different data. The traditional route is five fixture files, ordered and maintained by hand. Generating it from the schema looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# test-setup.sh&lt;/span&gt;
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# Truncate all tables (clean slate)&lt;/span&gt;
psql &lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"TRUNCATE users, products, categories, orders, order_items, payments, cart_items CASCADE"&lt;/span&gt;

&lt;span class="c"&gt;# Generate fresh data for this test run&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 10 users, 50 products across 5 categories, 20 orders with order items and payments"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; plain

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your Playwright tests query for data instead of assuming it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Instead of: cy.visit('/users/42/orders')&lt;/span&gt;
&lt;span class="c1"&gt;// Do this:&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user can view their order history&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Query for a user that has orders (generated data guarantees this)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users?has_orders=true&amp;amp;limit=1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/orders`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order-row&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;toHaveCount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;above&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test carries no hardcoded IDs and no assumption about order counts; it verifies that a user with orders can see their history, never a specific value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario-Specific Seeding
&lt;/h2&gt;

&lt;p&gt;Different test files seed different scenarios. The scope is just a string; describe what you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# For testing empty states&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"3 users with no orders"&lt;/span&gt;

&lt;span class="c"&gt;# For testing pagination&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"1 user with 50 orders"&lt;/span&gt;

&lt;span class="c"&gt;# For testing search and filtering&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"100 products across 10 categories with varied prices from 5 to 500 dollars"&lt;/span&gt;

&lt;span class="c"&gt;# For testing admin dashboards&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"50 users with mixed roles: 2 admins, 5 managers, 43 regular users, each with activity logs"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each scope produces data matching your current schema. You never spell out column names, types, or foreign-key values; the schema defines them and Seedfast fills the rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD Integration: Seed, Test, Clean
&lt;/h2&gt;

&lt;p&gt;This fits a CI pipeline, the same pattern as &lt;a href="https://seedfa.st/blog/synthetic-data-ci-cd" rel="noopener noreferrer"&gt;reseeding the CI database with synthetic data&lt;/a&gt; before every run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;E2E Tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;e2e&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testdb&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5432:5432&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
          &lt;span class="s"&gt;--health-cmd pg_isready&lt;/span&gt;
          &lt;span class="s"&gt;--health-interval 10s&lt;/span&gt;
          &lt;span class="s"&gt;--health-timeout 5s&lt;/span&gt;
          &lt;span class="s"&gt;--health-retries 5&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Apply migrations&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run db:migrate&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://test:test@localhost:5432/testdb&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Seed test data&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seedfast seed --scope "10 users with orders, products across categories, and reviews" --output plain&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SEEDFAST_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEEDFAST_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://test:test@localhost:5432/testdb&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run E2E tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://test:test@localhost:5432/testdb&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload test report&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playwright-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playwright-report&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline stays simple, applying the current schema, generating matching data, and running the tests. When a migration changes the schema the generated data changes with it, so the pipeline stays green without a fixture PR. The &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD database seeding guide&lt;/a&gt; covers the full setup, and &lt;a href="https://seedfa.st/blog/github-actions-seed-postgres-database" rel="noopener noreferrer"&gt;the service-container and health-check mechanics behind that YAML&lt;/a&gt; get their own walkthrough.&lt;/p&gt;

&lt;p&gt;For isolation between test files, truncate and reseed between groups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;globalSetup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./tests/global-setup.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;globalTeardown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./tests/global-teardown.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;


&lt;span class="c1"&gt;// tests/global-setup.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;globalSetup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Truncate and reseed before the entire suite&lt;/span&gt;
  &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;psql $DATABASE_URL -c "TRUNCATE users, orders, products CASCADE"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inherit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;seedfast seed --scope "10 users with orders and reviews, 50 products across 5 categories" --output plain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inherit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seeding runs once for the whole suite and does not repeat before each test. A run takes seconds, sometimes a few minutes on larger volumes or schemas, which suits a per-suite setup step but would drag before every test. Structure your tests to read whatever data is present, and per-test seeding never comes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Factory Pattern vs. Fixture Files vs. Generated Data
&lt;/h2&gt;

&lt;p&gt;Three approaches are common, each with trade-offs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Fixture files&lt;/th&gt;
&lt;th&gt;Factory pattern&lt;/th&gt;
&lt;th&gt;Generated data (Seedfast)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance on migration&lt;/td&gt;
&lt;td&gt;Manual edit every change&lt;/td&gt;
&lt;td&gt;Update the factory every change&lt;/td&gt;
&lt;td&gt;Zero — re-reads the schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema drift&lt;/td&gt;
&lt;td&gt;Drifts silently&lt;/td&gt;
&lt;td&gt;Drifts less, still drifts&lt;/td&gt;
&lt;td&gt;None — schema is the source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Foreign-key handling&lt;/td&gt;
&lt;td&gt;Manual ordering&lt;/td&gt;
&lt;td&gt;Coded per relationship&lt;/td&gt;
&lt;td&gt;Automatic dependency order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test isolation&lt;/td&gt;
&lt;td&gt;Poor (shared state)&lt;/td&gt;
&lt;td&gt;Good (per-test data)&lt;/td&gt;
&lt;td&gt;Good (fresh per run)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Determinism&lt;/td&gt;
&lt;td&gt;High (fixed values)&lt;/td&gt;
&lt;td&gt;High (seeded sequences)&lt;/td&gt;
&lt;td&gt;Low — assert on shape, not values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup speed&lt;/td&gt;
&lt;td&gt;Milliseconds&lt;/td&gt;
&lt;td&gt;Milliseconds&lt;/td&gt;
&lt;td&gt;Seconds (network call)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scales past ~20 tables&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Painful&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Fixture Files (JSON, SQL, YAML)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fixtures/users.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin@test.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"department_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user@test.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"department_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; They’re simple to understand, deterministic, and fast to load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; They drift from the schema and need manual maintenance on every migration; hardcoded IDs create coupling, foreign keys have to be ordered by hand, and the approach stops scaling past a dozen tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaks when:&lt;/strong&gt; Any schema change that adds a &lt;code&gt;NOT NULL&lt;/code&gt; column, moves a foreign key, or alters a constraint will break it, as will two fixture files that disagree about shared state (e.g., both reference department ID 1 but expect different names).&lt;/p&gt;

&lt;h3&gt;
  
  
  Factory Pattern (Factory Bot, Fishery, test containers)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// factories/user.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Factory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fishery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;sequence&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`user-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@test.com`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`User &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;departmentFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Being programmatic, factories handle relationships and let each test build its own data, which keeps isolation clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; You write and maintain one per model, updating it on every schema change. Complex relationships (polymorphic associations, multi-level nesting) get messy, and the factory code is one more representation of your schema that can drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaks when:&lt;/strong&gt; Factories fall out of sync with migrations, though less often than fixtures since the code sits closer to the model. Tables without a corresponding model (join tables, audit logs, materialized views) still slip through.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI-Generated Data (Seedfast)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"10 users with orders across 5 product categories"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where a tool like this lands among its AI-labeled peers is what &lt;a href="https://seedfa.st/blog/best-ai-test-data-generator" rel="noopener noreferrer"&gt;the best AI test data generator&lt;/a&gt; comparison sorts out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Maintenance drops to zero because it reads the actual schema and handles foreign keys, constraints, and ordering on its own; schema changes appear on the next run, and it works at any table count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Its data is non-deterministic, so assertions check structural properties, looking for “user has at least one order” and never “user 42 has order 101”. It needs a running database and network access to Seedfast’s backend, and loads slower than a fixture file (seconds vs. milliseconds). On a sensitive codebase, note that generating the data sends your table and column definitions (never the row values) to an external AI service, so a regulated project should clear that path against its data-governance policy first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaks when:&lt;/strong&gt; It breaks if Seedfast’s backend goes unavailable, though API-key caching and retry logic soften that. The bigger cost is restructuring an existing suite around data-shape assertions, which is genuine migration work.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use What
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Unit tests with no database&lt;/strong&gt; — Factory pattern or in-memory fakes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration tests (few tables)&lt;/strong&gt; — Factory pattern&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E2E tests (full schema)&lt;/strong&gt; — Generated data&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance/load testing&lt;/strong&gt; — Generated data at scale&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Existing suite with hundreds of fixture files&lt;/strong&gt; — Gradual migration, with generated data for new tests and factories for legacy&lt;/p&gt;

&lt;p&gt;The approaches aren’t mutually exclusive; many teams pair factories for unit and integration tests with generated data for E2E and performance runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Tests Data-Shape-Aware
&lt;/h2&gt;

&lt;p&gt;The biggest shift is how tests reference data. Where a fixture-bound test asserts on known values, a data-shape test queries for records with the properties it needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before (fixture-dependent):
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin can delete a user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Assumes fixture user with ID 1 is an admin&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loginAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// Assumes fixture user with ID 2 exists and is deletable&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin/users/2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-testid="delete-user"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User deleted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  After (data-shape-aware):
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin can delete a user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Find any admin user&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;admins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users?role=admin&amp;amp;limit=1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;admin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;admins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;// Find any non-admin user&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users?role=user&amp;amp;limit=1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loginAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/admin/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-testid="delete-user"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User deleted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version is more resilient. It doesn’t care which user happens to be the admin or which one gets deleted; it tests the behavior that an admin can delete a non-admin user, and that holds up through schema changes, data reseeds, and parallel execution.&lt;/p&gt;

&lt;p&gt;Yes, it’s more code, and it earns its keep — the old version only ever proved that one specific sequence of clicks kept working against one specific dataset, which is a much smaller promise than the feature actually makes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Pragmatic Migration Path
&lt;/h2&gt;

&lt;p&gt;If your E2E suite is built on fixtures, you don’t need to rewrite it all at once:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1:&lt;/strong&gt; Add a seeding step to CI alongside your existing fixtures, and run both together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;- name: Load legacy fixtures
  run: psql &lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; fixtures/seed.sql

- name: Seed additional data
  run: seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"seed 20 extra users with varied roles and order histories"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; plain
  &lt;span class="nb"&gt;env&lt;/span&gt;:
    SEEDFAST_API_KEY: &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;{ secrets.SEEDFAST_API_KEY &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Week 2-4:&lt;/strong&gt; Write new tests using the data-shape-aware pattern, and leave the old ones alone for now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Month 2:&lt;/strong&gt; Find the fixture files that break most often (git blame shows which get updated every sprint) and migrate those first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Month 3+:&lt;/strong&gt; As old fixture-dependent tests break from schema changes, rewrite them with the generated pattern and drop the fixture.&lt;/p&gt;

&lt;p&gt;Over time the fixture directory shrinks. New tests skip it, old ones migrate out as they break, and one &lt;code&gt;seedfast seed&lt;/code&gt; command becomes the only setup left.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Gets You
&lt;/h2&gt;

&lt;p&gt;Fewer flaky tests is what you notice first; underneath, your team’s whole approach to test data changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema changes stop breaking tests.&lt;/strong&gt; A migration adds a column, the next run already includes it, and no one files a fixture-update PR to catch up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test isolation becomes the default.&lt;/strong&gt; Every CI run starts from a fresh database with no shared state to inherit, so the suite parallelizes cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New developers can run the suite immediately.&lt;/strong&gt; Nobody hands them a database dump or a seed script from someone’s head; they clone, run migrations, seed, and it’s ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your tests end up documenting the feature itself.&lt;/strong&gt; A test that reads “a user with orders can view their order history” checks that claim against any valid data, and at that point the test reads as a small specification of the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why do my Playwright or Cypress tests fail after a database migration?
&lt;/h3&gt;

&lt;p&gt;The fixture they load no longer matches the migrated schema. Nothing flags a checked-in file when it falls behind, so the failure surfaces as an insert error or an unexpected null. Look in the fixture file, well upstream of the assertion that fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between test fixtures, factories, and generated test data?
&lt;/h3&gt;

&lt;p&gt;Fixtures are static files (JSON, SQL, YAML) you edit by hand on every schema change, while a factory keeps that record-building in code and still needs updating whenever a migration lands. Generated data reads the live schema at run time, picking up changes automatically with no upkeep, at the cost of being non-deterministic.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you generate test data for E2E tests without fixture files?
&lt;/h3&gt;

&lt;p&gt;Point a generation step at the schema so there’s no fixture file in the loop. Seedfast reads the live tables and inserts rows in the order the foreign keys require, so one seed step replaces the whole fixture directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you keep E2E tests isolated so they can run in parallel?
&lt;/h3&gt;

&lt;p&gt;Give each run its own fresh data so nothing is shared between files, and drop the ordering assumptions that make one test wait on another. When every test sets up, runs, and tears down its own records, nothing is left to fight over, and parallel execution follows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can you seed a test database in a CI pipeline?
&lt;/h3&gt;

&lt;p&gt;Yes, a seed step goes in after migrations and before the test run in GitHub Actions, GitLab CI, or any pipeline, with the CLI pointed at the throwaway CI database and a non-interactive flag so nothing waits on input. Because it calls an external service, regulated teams should clear it with whoever owns data-governance policy first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should you use a production database dump for E2E test data?
&lt;/h3&gt;

&lt;p&gt;Usually not, for the two reasons production dumps are risky anywhere. The moment a dump leaves production it carries real customer records into an environment with weaker access controls, and it starts aging as soon as the schema shifts. Generating data from the schema avoids both, with no real rows to guard and nothing to go stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/small-data-big-lies" rel="noopener noreferrer"&gt;Small Data, Big Lies: 6 Bugs Only Real Test Data Catches&lt;/a&gt; — why a tiny fixture database hides the failures that only appear with realistic data&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;Your Staging Database Is a Compliance Violation Waiting to Happen&lt;/a&gt; — generating realistic data instead of copying and anonymizing a production dump&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/migration-testing" rel="noopener noreferrer"&gt;ALTER TABLE, 5 Million Rows, and the Deploy That Took Down the Site&lt;/a&gt; — the other side of schema change: testing migrations against production-scale data&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/migration-review" rel="noopener noreferrer"&gt;Review SQL Migrations in 30 Seconds&lt;/a&gt; — catching the data-shape bugs a migration introduces before they reach your tests&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/test-data-postgresql" rel="noopener noreferrer"&gt;PostgreSQL Test Data: A Syntax Cookbook&lt;/a&gt; — the raw SQL patterns behind generating volume and realistic distributions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/seed-file-maintenance" rel="noopener noreferrer"&gt;Database Seed File Maintenance: Stop Patching seed.sql&lt;/a&gt; — the same drift problem on the seed-script side instead of the fixture side&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/data-seeding-tools" rel="noopener noreferrer"&gt;Data Seeding Tools Compared&lt;/a&gt; — where fixtures, factories, and schema-aware generators fit among the other approaches&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/agentic-qa-test-data" rel="noopener noreferrer"&gt;Agentic QA Test Data&lt;/a&gt; — why even AI agents that write and run your E2E tests still don't generate the data underneath them&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/pytest-database-fixtures" rel="noopener noreferrer"&gt;Pytest Database Fixtures That Stay Fast and Honest&lt;/a&gt; — the backend/integration-test counterpart to this page: fixture scopes, the SQLAlchemy SAVEPOINT rollback pattern, and seeding a Postgres baseline once for pytest&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Get started with Seedfast&lt;/a&gt; — connect a database and seed it for your next test run&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Generate E2E test data straight from your schema
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Get Started&lt;/a&gt; | &lt;a href="https://seedfa.st/docs" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; | &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Point Seedfast at the schema once, and keeping test fixtures in sync stops being your team’s problem.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/e2e-test-fixtures" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>javascript</category>
      <category>database</category>
      <category>programming</category>
    </item>
    <item>
      <title>Seed an Empty Postgres Database Without Leaving Claude Code</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:33:46 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/seed-an-empty-postgres-database-without-leaving-claude-code-150</link>
      <guid>https://dev.to/mikh-shytsko/seed-an-empty-postgres-database-without-leaving-claude-code-150</guid>
      <description>&lt;p&gt;You have a feature branch open in Claude Code and the integration tests are ready to run, but the dev database behind them is empty. Claude Code MCP database seeding closes that last gap without a detour, letting the agent already sitting in your editor fill the schema with valid rows before a single test fires. Ask the model to write a seed script instead and you tend to get inserts whose foreign keys point at rows that were never created, a failure the &lt;a href="https://seedfa.st/blog/generate-test-data-with-ai" rel="noopener noreferrer"&gt;generate test data with AI&lt;/a&gt; playbook walks through in depth.&lt;/p&gt;

&lt;p&gt;This page stays deliberately narrow. One config block, one prompt, and an empty Postgres database comes back populated, all from the tool you already keep open. I run this a dozen times a day against branch databases, so what follows is the exact path from zero to seeded, not &lt;a href="https://seedfa.st/blog/mcp-test-data" rel="noopener noreferrer"&gt;the full map of MCP servers for test data&lt;/a&gt; you could wire up instead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Seedfast runs as an MCP server, so an agent inside Claude Code populates a Postgres database in one call. It reads your live schema, generates relational rows with valid foreign keys, and writes them back, without you leaving the editor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How Claude Code MCP database seeding works
&lt;/h2&gt;

&lt;p&gt;When Claude Code starts, it reads your project's &lt;code&gt;.mcp.json&lt;/code&gt; and launches the Seedfast server through &lt;code&gt;npx&lt;/code&gt;, which then advertises a small set of tools over the Model Context Protocol. The one that matters here is &lt;code&gt;seedfast_run&lt;/code&gt;. You hand the agent a scope in plain English and it forwards that scope to &lt;code&gt;seedfast_run&lt;/code&gt;. From there the engine reads your live Postgres schema and generates data that's valid and connected before it writes a single row, so nothing ends up pointing at a row that isn't there.&lt;/p&gt;

&lt;p&gt;That division of labor keeps the data coherent, since the agent stays in the part it does well, describing intent and reasoning about your feature, and hands the relational bookkeeping to code that reads the real schema rather than guessing at it. Keeping every row valid, including tables that reference themselves or each other, belongs to the seeder, while enforcement of &lt;a href="https://seedfa.st/blog/referential-integrity" rel="noopener noreferrer"&gt;referential integrity&lt;/a&gt; never leaves the database. The deeper argument for why a language model loses the relational thread lives in the same &lt;a href="https://seedfa.st/blog/generate-test-data-with-ai" rel="noopener noreferrer"&gt;playbook&lt;/a&gt;, so I won't relitigate it here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up Seedfast in .mcp.json
&lt;/h2&gt;

&lt;p&gt;Two things have to exist before the agent can seed anything, a Seedfast account and an API key, so log in at seedfa.st, open &lt;strong&gt;Settings → API Keys&lt;/strong&gt; , and create one; it comes back in the &lt;code&gt;sfk_live_...&lt;/code&gt; format. Treat that key like any other credential and keep the real value out of version control, because anything you commit to &lt;code&gt;.mcp.json&lt;/code&gt; travels with the repo. Teams that check the file in usually commit it with the placeholder and paste the real key only into their local copies.&lt;/p&gt;

&lt;p&gt;Add Seedfast to the project's &lt;code&gt;.mcp.json&lt;/code&gt; at the repository root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"seedfast"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"seedfast@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"SEEDFAST_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sfk_live_your_api_key_here"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll need Node.js 18 or newer on the machine, since the server runs through &lt;code&gt;npx&lt;/code&gt;. Restart Claude Code so it picks up the new config, then confirm the wiring by asking the agent to run &lt;code&gt;seedfast_doctor&lt;/code&gt;, which reports whether the CLI is healthy and your key authenticated. If you're setting up Cursor, VS Code, or Claude Desktop instead, or you hit a snag along the way, the &lt;a href="https://seedfa.st/docs/mcp-setup-guide" rel="noopener noreferrer"&gt;MCP setup guide&lt;/a&gt; carries the config for every client and the troubleshooting steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  One prompt, a seeded database
&lt;/h2&gt;

&lt;p&gt;With the server wired up, a real session reads like a sentence. Say you're building order history and the tests need customers who actually own something. You would type roughly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seed customers with related orders and line items, a few rows each

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code reads that, calls &lt;code&gt;seedfast_run&lt;/code&gt; with the scope, and the engine seeds the three tables so every order points at a real customer and every line item at a real order. And because scope controls scale as much as shape, you ask for a few rows each when a unit test just needs something to assert against, and for thousands when you want to watch a query plan buckle under load.&lt;/p&gt;

&lt;p&gt;How long it takes tracks how much you asked for. A single table lands in five to fifteen seconds, five to ten related tables in thirty to sixty, and a full schema in two to five minutes, so the order-history seed above finishes while you're still reading the test you're about to run. Once it's done, ask the agent for the run summary and it reports which tables succeeded along with the total rows written, or run a quick &lt;code&gt;SELECT count(*)&lt;/code&gt; yourself and read the numbers straight from the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Point it at a dev database, never production
&lt;/h2&gt;

&lt;p&gt;There is an honest worry sitting underneath all of this. You are handing an agent a write path into a database, which is a reasonable thing to feel cautious about, so the single rule that keeps it boring is to point Seedfast at a dev or branch database only, the same discipline you would already apply to any seed script a teammate wrote. The blast radius stays small for a structural reason too, since Seedfast needs no production data to do its job; it reads the schema and writes fresh rows generated from scratch, so nothing about your real customer records has to come anywhere near the run.&lt;/p&gt;

&lt;p&gt;That same seed call drops straight into CI, where the MCP wrapper gives way to the plain CLI. The pipeline version of the workflow, fresh rows for every pull request with no hand-maintained &lt;code&gt;seed.sql&lt;/code&gt; in sight, has its own write-up in &lt;a href="https://seedfa.st/blog/synthetic-data-ci-cd" rel="noopener noreferrer"&gt;synthetic data for CI/CD&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Claude Code support MCP servers like Seedfast?
&lt;/h3&gt;

&lt;p&gt;Claude Code, Anthropic's agentic coding tool, supports MCP servers natively and reads their configuration from a project-scoped &lt;code&gt;.mcp.json&lt;/code&gt; at the repository root. Once you add an &lt;code&gt;mcpServers&lt;/code&gt; block naming the command to launch, Claude Code starts that server at boot and its tools, Seedfast's included, become available to the agent mid-conversation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What can I ask Claude Code to seed with Seedfast?
&lt;/h3&gt;

&lt;p&gt;Seedfast can seed anything from a single table to every table across every schema, described to Claude Code in plain language rather than SQL. A scope such as "seed customers with related orders and line items" populates one connected slice, while "seed all tables in all schemas" fills the whole database. Because the engine re-reads your live schema on each run, a table you added this morning flows through without extra wiring. Sensitive tables stay out when you name them as exclusions in the scope.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does a seed run take from Claude Code?
&lt;/h3&gt;

&lt;p&gt;A seed run from Claude Code usually finishes in well under a minute, and even a full schema of fifty-plus tables stays inside a few minutes. Expect five to fifteen seconds when a single table is in scope and two to five minutes at the everything-at-once end, which is why scoping the seed to the feature at hand keeps the wait short.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the same MCP setup work in Cursor or VS Code?
&lt;/h3&gt;

&lt;p&gt;Yes, the same server block works unchanged in Cursor, VS Code, Claude Desktop, and Claude Code, because each client launches Seedfast through the identical &lt;code&gt;npx&lt;/code&gt; command. Only the file it lives in moves around, with Cursor reading &lt;code&gt;.cursor/mcp.json&lt;/code&gt; and VS Code keeping the block in its own MCP settings. The &lt;a href="https://seedfa.st/docs/mcp-setup-guide" rel="noopener noreferrer"&gt;MCP setup guide&lt;/a&gt; spells out the location for each client, and Codex CLI takes the same &lt;code&gt;npx&lt;/code&gt; command in TOML form, covered in &lt;a href="https://seedfa.st/blog/codex-cli-database-seeding" rel="noopener noreferrer"&gt;its own walkthrough&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seed the empty database from where you already work
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; turns the empty-database step into one line inside Claude Code, reading your live schema and writing valid, connected rows off it with no production data involved. Start on the &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;free plan&lt;/a&gt;, no card required, and the first rows usually land a couple of minutes after the config block goes in.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/claude-code-mcp-database-seeding" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>postgres</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Seed a Neon Database: psql, Prisma, Drizzle</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:33:11 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/how-to-seed-a-neon-database-psql-prisma-drizzle-26fe</link>
      <guid>https://dev.to/mikh-shytsko/how-to-seed-a-neon-database-psql-prisma-drizzle-26fe</guid>
      <description>&lt;p&gt;&lt;strong&gt;To seed a Neon database, use the unpooled connection string with &lt;code&gt;psql&lt;/code&gt;, Prisma, or Drizzle. The pooled URL breaks prepared statements mid-seed.&lt;/strong&gt; 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 (&lt;code&gt;psql&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

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

&lt;p&gt;&lt;strong&gt;Quick fix if you landed here from a broken seed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 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

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You provisioned Neon because it &lt;a href="https://neon.com/docs/introduction/serverless" rel="noopener noreferrer"&gt;spins up in milliseconds&lt;/a&gt;, 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.&lt;/p&gt;

&lt;p&gt;We'll cover all three, starting with raw SQL, then Prisma and Drizzle seed scripts with Neon's serverless driver, then how &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; turns a plain-English scope into connected rows without a seed file to write. If you want the general PostgreSQL version first, &lt;a href="https://seedfa.st/blog/seed-database" rel="noopener noreferrer"&gt;how to seed a database&lt;/a&gt; covers the cross-framework fundamentals; this article is specifically about Neon.&lt;/p&gt;

&lt;h2&gt;
  
  
  When do you need to seed a Neon database?
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Three scenarios force the question of how to seed a Neon database:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;New project onboarding.&lt;/strong&gt; A developer clones the repo, creates a Neon project, and runs the migrations; the tables exist, but nothing else does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview branches per PR.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staging and demo environments.&lt;/strong&gt; You need 500 products and realistic order histories to show someone what the app looks like with data in it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://neon.com/docs/guides/branching-intro" rel="noopener noreferrer"&gt;Neon's branching docs&lt;/a&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connection strings: pooled, unpooled, and when each matters
&lt;/h2&gt;

&lt;p&gt;Neon gives every branch two connection strings. You can copy them from the Neon dashboard under &lt;strong&gt;Connection Details&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 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

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;-pooler&lt;/code&gt; in the pooled hostname. That's &lt;a href="https://neon.com/docs/connect/connection-pooling" rel="noopener noreferrer"&gt;PgBouncer in transaction mode&lt;/a&gt;, 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 &lt;strong&gt;not&lt;/strong&gt; what you want for seeding, because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prepared statements don't survive the transaction boundary&lt;/li&gt;
&lt;li&gt;Long-running &lt;code&gt;COPY&lt;/code&gt; operations and large transactional seeds can hit statement timeouts&lt;/li&gt;
&lt;li&gt;Some ORMs emit session-level &lt;code&gt;SET&lt;/code&gt; statements that get discarded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For seeding, migrations, and any admin script, use the &lt;strong&gt;unpooled&lt;/strong&gt; string. Export it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env.local
DATABASE_URL="postgresql://...ep-xxxx-pooler.../dbname?sslmode=require" # app runtime
DIRECT_URL="postgresql://...ep-xxxx.../dbname?sslmode=require" # migrations + seeds

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prisma calls this &lt;code&gt;directUrl&lt;/code&gt; in &lt;code&gt;schema.prisma&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: Seed Neon database with raw SQL
&lt;/h2&gt;

&lt;p&gt;The simplest thing that works is writing INSERTs and running them with &lt;code&gt;psql&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- seed.sql&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;teams&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Engineering'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Design'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;NOTHING&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bob@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;NOTHING&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="n"&gt;psql&lt;/span&gt; &lt;span class="nv"&gt;"$DIRECT_URL"&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SSL is mandatory on Neon. The &lt;code&gt;sslmode=require&lt;/code&gt; query parameter in the connection string handles it automatically. If you get &lt;code&gt;FATAL: connection requires SSL&lt;/code&gt;, your URL is missing it.&lt;/p&gt;

&lt;p&gt;When you have tens of thousands of rows to load, &lt;code&gt;COPY FROM STDIN&lt;/code&gt; 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIRECT_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"COPY products (name, price, category_id) FROM STDIN CSV"&lt;/span&gt; &amp;lt; products.csv

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt; keeps the seed idempotent so CI reruns don't fail on the second attempt. For data that should reflect the latest values, use &lt;code&gt;ON CONFLICT DO UPDATE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;feature_flags&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'new_checkout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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. &lt;a href="https://seedfa.st/blog/seed-file-maintenance" rel="noopener noreferrer"&gt;Seed file maintenance&lt;/a&gt; covers why this lifecycle is so brutal on active codebases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: Seed Neon database with an ORM
&lt;/h2&gt;

&lt;p&gt;Most Neon projects run through Prisma, Drizzle, or Kysely. Each has its own seeding path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prisma + Neon
&lt;/h3&gt;

&lt;p&gt;Prisma on a Node.js server works with Neon out of the box via the standard &lt;code&gt;pg&lt;/code&gt; driver. Edge runtimes (Vercel Edge, Cloudflare Workers) require the Neon serverless driver adapter.&lt;/p&gt;

&lt;p&gt;In a regular Node.js seed script, just point Prisma at the unpooled URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// prisma/seed.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Engineering&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Engineering&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alice@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alice@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$disconnect&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;


&lt;span class="c1"&gt;// prisma.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prisma/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prisma/schema.prisma&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;migrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prisma/migrations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tsx prisma/seed.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;datasource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DIRECT_URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;


&lt;span class="nx"&gt;npx&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you deploy on Vercel Edge or Cloudflare Workers, the runtime has no TCP sockets. Install the &lt;a href="https://www.prisma.io/docs/orm/overview/databases/neon" rel="noopener noreferrer"&gt;Prisma Neon adapter&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;prisma&lt;/span&gt;&lt;span class="sr"&gt;/adapter-neon @neondatabase/&lt;/span&gt;&lt;span class="nx"&gt;serverless&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt;


&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaNeon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/adapter-neon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;neonConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@neondatabase/serverless&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;neonConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webSocketConstructor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaNeon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drizzle + Neon
&lt;/h3&gt;

&lt;p&gt;Drizzle ships two Neon-specific packages: &lt;code&gt;drizzle-orm/neon-http&lt;/code&gt; for one-shot HTTP queries, which suits serverless app code but &lt;strong&gt;not&lt;/strong&gt; seed transactions, and &lt;code&gt;drizzle-orm/neon-serverless&lt;/code&gt; for WebSocket sessions with full transaction support, the one you want for seeds.&lt;/p&gt;

&lt;p&gt;The simplest seed path is the node-postgres driver against the unpooled URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/seed.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;drizzle&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drizzle-orm/node-postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../src/db/schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DIRECT_URL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;drizzle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Engineering&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onConflictDoUpdate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Engineering&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;returning&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alice@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onConflictDoNothing&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;


&lt;span class="nx"&gt;tsx&lt;/span&gt; &lt;span class="nx"&gt;scripts&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer the Neon serverless driver for consistency with the rest of your codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;drizzle&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drizzle-orm/neon-serverless&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;neonConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@neondatabase/serverless&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;neonConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webSocketConstructor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DIRECT_URL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;drizzle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either works, since the &lt;code&gt;pg&lt;/code&gt; version is one less dependency while the serverless-driver version keeps your app and seed script on the same driver.&lt;/p&gt;

&lt;p&gt;Both Prisma and Drizzle share the same ORM seed problem, since the values are hand-written. When a migration adds a &lt;code&gt;NOT NULL organization_id&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using @neondatabase/serverless with local Postgres for development
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://neon.com/docs/serverless/serverless-driver" rel="noopener noreferrer"&gt;&lt;code&gt;@neondatabase/serverless&lt;/code&gt;&lt;/a&gt; cannot talk to a regular local Postgres on &lt;code&gt;localhost:5432&lt;/code&gt;. The HTTP driver (&lt;code&gt;neon()&lt;/code&gt;) speaks HTTP to Neon's proxy, and the WebSocket driver (&lt;code&gt;Pool&lt;/code&gt; from &lt;code&gt;@neondatabase/serverless&lt;/code&gt;) 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.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;@neondatabase/serverless&lt;/code&gt;.&lt;/p&gt;

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/db/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;drizzle&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;drizzleNeon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drizzle-orm/neon-http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;drizzle&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;drizzlePg&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drizzle-orm/node-postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;neon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@neondatabase/serverless&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;drizzleNeon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;neon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;drizzlePg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Local &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL="postgresql://user:pass@ep-xxxx.region.aws.neon.tech/dbname?sslmode=require"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;It comes down to one process-env flip and two factories, and the query code stays identical either way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 2: local WebSocket proxy for @neondatabase/serverless
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;If you need a single code path that uses &lt;code&gt;@neondatabase/serverless&lt;/code&gt; everywhere, run a local WebSocket-to-Postgres proxy. Neon's &lt;a href="https://github.com/neondatabase/wsproxy" rel="noopener noreferrer"&gt;&lt;code&gt;wsproxy&lt;/code&gt;&lt;/a&gt; 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.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Minimal &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;wsproxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/neondatabase/wsproxy:latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;APPEND_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres:5432"&lt;/span&gt;
      &lt;span class="na"&gt;ALLOW_ADDR_REGEX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.*"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4444:80"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point &lt;code&gt;@neondatabase/serverless&lt;/code&gt; at the local proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;neonConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@neondatabase/serverless&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;neonConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webSocketConstructor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;neonConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wsProxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`localhost:4444/v2`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;neonConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useSecureWebSocket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;neonConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pipelineTLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;neonConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pipelineConnect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgresql://postgres:postgres@localhost:5432/postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  drizzle-kit push local postgres "neon serverless" websocket warning
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;drizzle-kit push&lt;/code&gt; and &lt;code&gt;drizzle-kit migrate&lt;/code&gt; always use &lt;code&gt;node-postgres&lt;/code&gt; under the hood. The warning means your &lt;code&gt;drizzle.config.ts&lt;/code&gt; 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 &lt;code&gt;pg&lt;/code&gt;-compatible URL fixes it, Neon's unpooled direct connection for production and plain local Postgres for development.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// drizzle.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drizzle-kit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/db/schema.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./drizzle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dialect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgresql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dbCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL_UNPOOLED&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgresql://postgres:postgres@localhost:5432/postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Neon, &lt;code&gt;DATABASE_URL_UNPOOLED&lt;/code&gt; is the hostname without &lt;code&gt;-pooler&lt;/code&gt; and with &lt;code&gt;?sslmode=require&lt;/code&gt;. Locally, plain &lt;code&gt;postgresql://postgres:postgres@localhost:5432/postgres&lt;/code&gt; is enough. Do not try to silence the warning by setting &lt;code&gt;driver: 'pg'&lt;/code&gt; plus a serverless URL. The underlying mismatch is the connection string, not the config key. (Drizzle-kit removed the &lt;code&gt;driver&lt;/code&gt; field altogether in &lt;a href="https://orm.drizzle.team/kit-docs/conf" rel="noopener noreferrer"&gt;0.21+&lt;/a&gt;, making the workaround a no-op on newer versions.)&lt;/p&gt;

&lt;h3&gt;
  
  
  drizzle-orm/neon-http vs drizzle-orm/neon-serverless vs drizzle-orm/node-postgres
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Three drivers speak three different wire protocols behind one Drizzle query API.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Driver&lt;/th&gt;
&lt;th&gt;Wire protocol&lt;/th&gt;
&lt;th&gt;Transactions&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Works against&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drizzle-orm/neon-http&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTPS to Neon proxy&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Node + edge&lt;/td&gt;
&lt;td&gt;Neon only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drizzle-orm/neon-serverless&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WebSocket to Neon&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Node + edge&lt;/td&gt;
&lt;td&gt;Neon only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drizzle-orm/node-postgres&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TCP wire protocol&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Node only&lt;/td&gt;
&lt;td&gt;Any Postgres (including local)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pick by deployment target:&lt;/p&gt;

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

&lt;p&gt;The conditional-import pattern from Solution 1 above lets you mix and match: &lt;code&gt;node-postgres&lt;/code&gt; locally for &lt;code&gt;pnpm dev&lt;/code&gt;, &lt;code&gt;neon-http&lt;/code&gt; or &lt;code&gt;neon-serverless&lt;/code&gt; for the deployed app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: Schema-aware seeding with Seedfast
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;seed.sql&lt;/code&gt; or &lt;code&gt;seed.ts&lt;/code&gt; to maintain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; seedfast
&lt;span class="c"&gt;# or: brew install argon-it/tap/seedfast&lt;/span&gt;

&lt;span class="c"&gt;# Log in and connect to your Neon database&lt;/span&gt;
seedfast connect
&lt;span class="c"&gt;# Paste the Neon unpooled connection string when prompted&lt;/span&gt;

&lt;span class="c"&gt;# Generate data from the current schema&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"small engineering team with 3 projects and task assignments"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Different scopes for different environments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Local dev — minimal, fast&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"2 teams, 5 users, 10 products"&lt;/span&gt;

&lt;span class="c"&gt;# Preview branch for a feature PR&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"3 users with completed onboarding, 5 draft posts, 2 published"&lt;/span&gt;

&lt;span class="c"&gt;# Staging demo&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"500 realistic products, 50 users with 6 months of order history"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seedfast works alongside Prisma, Drizzle, Kysely, and plain &lt;code&gt;pg&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Generated rows land straight in your Neon branch, with no CSV to import, no intermediate file to load, and no manual &lt;code&gt;psql&lt;/code&gt; step at the end. Run the seed command once and the tables are full. See &lt;a href="https://seedfa.st/privacy-policy" rel="noopener noreferrer"&gt;data handling and privacy&lt;/a&gt; for exactly what crosses the wire.&lt;/p&gt;

&lt;p&gt;A free plan is available. &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Connect and run your first seed&lt;/a&gt; in about two minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate test data for a Neon branch (that survives a branch reset)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;After a branch reset, the thing that has to survive is the generator, not a &lt;code&gt;git&lt;/code&gt;-tracked seed file.&lt;/strong&gt; 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 &lt;a href="https://neon.com/docs/manage/branches" rel="noopener noreferrer"&gt;instantly reset a branch to its parent&lt;/a&gt;, which wipes every branch-local write. After a reset, you have to repopulate. With a static &lt;code&gt;seed.sql&lt;/code&gt; that's a &lt;code&gt;psql&lt;/code&gt; run; with a hand-edited ORM seed it's a &lt;code&gt;psql&lt;/code&gt; run plus whatever migrations have landed since you last touched the file.&lt;/p&gt;

&lt;p&gt;Two things make a Neon branch different from a plain database, and both favor reading the schema over replaying a file:&lt;/p&gt;

&lt;p&gt;A branch created off &lt;code&gt;main&lt;/code&gt; inherits &lt;code&gt;main&lt;/code&gt;'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 &lt;code&gt;main&lt;/code&gt;. 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 &lt;code&gt;seedfast connect&lt;/code&gt; steps are in Method 3 above):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# After resetting the branch — repopulate from its current schema&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"3 users with completed onboarding, 5 draft posts, 2 published"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping &lt;code&gt;neon branch seed data&lt;/code&gt; current becomes a property of the run instead of something stored in a file you maintain. Static &lt;code&gt;psql&lt;/code&gt;/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 &lt;a href="https://seedfa.st/blog/neon-branching-seed-data" rel="noopener noreferrer"&gt;Neon branch seeding&lt;/a&gt;; for the broader tool decision across Postgres hosts, the &lt;a href="https://seedfa.st/blog/best-postgres-test-data-generator" rel="noopener noreferrer"&gt;best Postgres test data generator&lt;/a&gt; comparison weighs the options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seed Neon once, branch many times
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;A typical workflow looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One-time setup on main branch&lt;/span&gt;
psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIRECT_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; seed.sql
&lt;span class="c"&gt;# or&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"realistic e-commerce dataset"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in CI, every pull request gets its own branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/preview.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Preview branch&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;preview&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create Neon branch from main&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neon&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neondatabase/create-branch-action@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;project_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NEON_PROJECT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;branch_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;preview/pr-${{ github.event.pull_request.number }}&lt;/span&gt;
          &lt;span class="na"&gt;parent_branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
          &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NEON_API_KEY }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run migrations on the new branch&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx prisma migrate deploy&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DIRECT_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.neon.outputs.db_url }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run E2E tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run test:e2e&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.neon.outputs.db_url_pooled }}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no seeding step in CI, because the branch already has data, inherited from &lt;code&gt;main&lt;/code&gt;. Branch creation takes about a second, so the PR pipeline isn't waiting on database provisioning.&lt;/p&gt;

&lt;p&gt;When the PR is closed or merged, a cleanup action deletes the branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/cleanup.yml&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;closed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;delete-branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neondatabase/delete-branch-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;project_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NEON_PROJECT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;preview/pr-${{ github.event.pull_request.number }}&lt;/span&gt;
          &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NEON_API_KEY }}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For the general CI case without branching, the approach in &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD database seeding&lt;/a&gt; applies to Neon too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Neon seeding issues and how to fix them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "prepared statement &lt;code&gt;s1&lt;/code&gt; already exists"
&lt;/h3&gt;

&lt;p&gt;You're seeding through the pooled (&lt;code&gt;-pooler&lt;/code&gt;) 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  "cached plan must not change result type"
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  "connection terminated unexpectedly" mid-seed
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  "SSL required"
&lt;/h3&gt;

&lt;p&gt;Your connection string is missing &lt;code&gt;?sslmode=require&lt;/code&gt;, and Neon rejects non-SSL connections outright. Add the query parameter or set &lt;code&gt;PGSSLMODE=require&lt;/code&gt; in the environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  "permission denied for schema public"
&lt;/h3&gt;

&lt;p&gt;Neon projects have one default role, the owner role (named for your database, e.g., &lt;code&gt;neondb_owner&lt;/code&gt;), 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  "too many connections"
&lt;/h3&gt;

&lt;p&gt;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 &lt;a href="https://neon.com/docs/manage/computes" rel="noopener noreferrer"&gt;Neon's compute docs&lt;/a&gt;). Close pools after seeding (&lt;code&gt;pool.end()&lt;/code&gt; for &lt;code&gt;pg&lt;/code&gt; / Drizzle, &lt;code&gt;prisma.$disconnect()&lt;/code&gt; for Prisma). For parallel seed scripts, serialize them or lower the pool size (&lt;code&gt;max: 5&lt;/code&gt; in &lt;code&gt;new Pool(...)&lt;/code&gt;). Use the direct URL for the seed process only.&lt;/p&gt;

&lt;h3&gt;
  
  
  "relation does not exist"
&lt;/h3&gt;

&lt;p&gt;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 &lt;code&gt;migrate → seed&lt;/code&gt;; it never runs in reverse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual SQL vs ORM vs Seedfast for Neon
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Raw SQL (&lt;code&gt;psql -f&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Prisma / Drizzle seed&lt;/th&gt;
&lt;th&gt;Seedfast&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup time&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Already there if you use the ORM&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install -g seedfast&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External dependency&lt;/td&gt;
&lt;td&gt;None — &lt;code&gt;psql&lt;/code&gt; is everywhere&lt;/td&gt;
&lt;td&gt;None — already in your stack&lt;/td&gt;
&lt;td&gt;Yes — separate CLI plus a network call to your DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File you maintain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;seed.sql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;seed.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None — reads the live schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FK order&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Survives migrations&lt;/td&gt;
&lt;td&gt;No — requires manual updates on schema changes&lt;/td&gt;
&lt;td&gt;No — requires manual updates on schema changes&lt;/td&gt;
&lt;td&gt;Yes — regenerates from the live schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realistic volumes&lt;/td&gt;
&lt;td&gt;Painful beyond ~50 rows&lt;/td&gt;
&lt;td&gt;Works with Faker, still manual&lt;/td&gt;
&lt;td&gt;Natural-language scope describes the dataset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works with Neon branches&lt;/td&gt;
&lt;td&gt;Yes (unpooled URL)&lt;/td&gt;
&lt;td&gt;Yes (unpooled URL)&lt;/td&gt;
&lt;td&gt;Yes (unpooled URL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good for static config (feature flags, country codes, roles)&lt;/td&gt;
&lt;td&gt;Excellent — versioned and reviewed&lt;/td&gt;
&lt;td&gt;Excellent — versioned and reviewed&lt;/td&gt;
&lt;td&gt;Not the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good for dev / CI / staging datasets&lt;/td&gt;
&lt;td&gt;Manual re-sync on every migration&lt;/td&gt;
&lt;td&gt;Manual re-sync on every migration&lt;/td&gt;
&lt;td&gt;Regenerates from the live schema&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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. &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Try it free on your Neon schema&lt;/a&gt;, which takes about two minutes and never asks for a credit card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I seed a Neon database from the command line?
&lt;/h3&gt;

&lt;p&gt;Copy the unpooled connection string from the Neon dashboard (the hostname without &lt;code&gt;-pooler&lt;/code&gt;) and run &lt;code&gt;psql "$DATABASE_URL" -f seed.sql&lt;/code&gt;. Include &lt;code&gt;?sslmode=require&lt;/code&gt; on the connection string. For Prisma projects, &lt;code&gt;npx prisma db seed&lt;/code&gt; runs your &lt;code&gt;prisma/seed.ts&lt;/code&gt;. For schemas with many tables or foreign keys, &lt;code&gt;seedfast seed --scope "..."&lt;/code&gt; generates connected data without a seed file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use the pooled or unpooled Neon URL for seeding?
&lt;/h3&gt;

&lt;p&gt;Use the &lt;strong&gt;unpooled&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Neon branches inherit seed data from the parent?
&lt;/h3&gt;

&lt;p&gt;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 &lt;code&gt;main&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I seed a Neon database in GitHub Actions?
&lt;/h3&gt;

&lt;p&gt;Create the branch with &lt;code&gt;neondatabase/create-branch-action&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I seed a Neon database with Prisma?
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;directUrl&lt;/code&gt; in &lt;code&gt;schema.prisma&lt;/code&gt; to Neon's unpooled connection string, keep &lt;code&gt;url&lt;/code&gt; pointing at the pooled one for app queries, write your &lt;code&gt;prisma/seed.ts&lt;/code&gt;, and run &lt;code&gt;npx prisma db seed&lt;/code&gt;. If you deploy on an edge runtime, also install &lt;code&gt;@prisma/adapter-neon&lt;/code&gt;, &lt;code&gt;@neondatabase/serverless&lt;/code&gt;, and &lt;code&gt;ws&lt;/code&gt;. But seeding itself usually runs in Node.js, not edge, so the adapter isn't required for the seed script.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I seed a Neon database with Drizzle?
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;drizzle-orm/node-postgres&lt;/code&gt; with the &lt;code&gt;pg&lt;/code&gt; driver and Neon's unpooled URL for the seed script. For serverless/edge app code, switch to &lt;code&gt;drizzle-orm/neon-http&lt;/code&gt; (one-off queries) or &lt;code&gt;drizzle-orm/neon-serverless&lt;/code&gt; (transactions over WebSocket). Seedfast can also seed Drizzle-managed schemas directly, since it talks to Postgres instead of going through the ORM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use @neondatabase/serverless with local Postgres for development?
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  What connection string does @neondatabase/serverless use for local Postgres?
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  What's the best @neondatabase/serverless alternative driver for local Drizzle development?
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  Why does drizzle-kit push warn about neon serverless WebSocket against local Postgres?
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  Why does my Neon seed work locally but fail in CI?
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  What's the best test data generator for a Neon branch?
&lt;/h3&gt;

&lt;p&gt;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 &lt;code&gt;psql&lt;/code&gt; or ORM seeds remain the right choice for static reference data that should live in git and survive a reset by being committed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I keep Neon branch seed data current when the schema changes?
&lt;/h3&gt;

&lt;p&gt;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 &lt;code&gt;seed.sql&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/seed-database" rel="noopener noreferrer"&gt;How to seed a database: PostgreSQL practical guide&lt;/a&gt;: the framework-agnostic version of this article, covering raw SQL, Prisma, Drizzle, TypeORM, and &lt;code&gt;node-postgres&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/database-seeding" rel="noopener noreferrer"&gt;Database seeding: methods and best practices&lt;/a&gt;: the conceptual companion covering reference vs test data, idempotency, and when seed files stop scaling&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/database-seeder" rel="noopener noreferrer"&gt;Database seeder tools compared&lt;/a&gt;: quick reference for Laravel, Prisma, Drizzle, TypeORM, and EF Core seeders next to standalone tools&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/seed-file-maintenance" rel="noopener noreferrer"&gt;Seed file maintenance&lt;/a&gt;: why static seed files fall out of sync with your schema and what to do about it&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/microservice-seeding" rel="noopener noreferrer"&gt;Microservice database seeding&lt;/a&gt;: when one Neon project isn't enough and you're seeding multiple databases that reference each other&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/supabase-db-seed" rel="noopener noreferrer"&gt;How to seed a Supabase database&lt;/a&gt;: the Supabase sibling to this guide, covering &lt;code&gt;seed.sql&lt;/code&gt;, &lt;code&gt;supabase db reset&lt;/code&gt;, &lt;code&gt;auth.users&lt;/code&gt;, and Supabase preview branches&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/compare/neosync-alternative" rel="noopener noreferrer"&gt;Neosync alternative&lt;/a&gt;: migration guide for teams who ran Neosync against Neon and are now orphaned after the September 2025 Grow Therapy acquisition&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/compare/snaplet-seed-alternative" rel="noopener noreferrer"&gt;Snaplet Seed alternative&lt;/a&gt;: migration guide for teams whose &lt;code&gt;@snaplet/seed&lt;/code&gt; setup stalled after the 2024 shutdown, with the move to schema-aware seeding&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Get started with Seedfast&lt;/a&gt;: connect to your Neon database and run your first schema-aware seed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/seed-neon-database" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>sql</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Circular Foreign Key Seed: Three Workarounds That Actually Run</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:32:36 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/circular-foreign-key-seed-three-workarounds-that-actually-run-57ga</link>
      <guid>https://dev.to/mikh-shytsko/circular-foreign-key-seed-three-workarounds-that-actually-run-57ga</guid>
      <description>&lt;p&gt;&lt;strong&gt;To seed two Postgres tables that reference each other, mark the foreign keys &lt;code&gt;DEFERRABLE INITIALLY IMMEDIATE&lt;/code&gt; and wrap the inserts in a transaction that issues &lt;code&gt;SET CONSTRAINTS ALL DEFERRED&lt;/code&gt; — Postgres then validates both FKs at &lt;code&gt;COMMIT&lt;/code&gt; instead of after each row.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- both foreign keys must already be declared DEFERRABLE (see Workaround 1)&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINTS&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;DEFERRED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_owner_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Acme'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Ada'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A circular foreign key seed is the case where two Postgres tables reference each other and your seed file has no valid first row to insert. You pick a parent, INSERT it, and Postgres replies with &lt;code&gt;violates foreign key constraint&lt;/code&gt;. You flip the order. Same error, from the other direction. Every ordering fails the constraint check the moment the row hits disk.&lt;/p&gt;

&lt;p&gt;This is a narrower problem than the generic foreign-key ordering one. The general FK case has a &lt;a href="https://seedfa.st/blog/test-data-postgresql" rel="noopener noreferrer"&gt;topological-sort answer&lt;/a&gt; — insert parents before children, the ordering &lt;a href="https://seedfa.st/blog/database-seeding" rel="noopener noreferrer"&gt;database seeding&lt;/a&gt; walks through. The circular case doesn't have a sort answer; it's a different shape of fix. This article walks through the three SQL patterns Postgres actually supports for this — deferred constraints, a nullable back-edge with a two-phase UPDATE, and a single-statement data-modifying CTE — then shows what happens when you point &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; at the same schema, and what doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A circular foreign key seed fails by default because Postgres checks each &lt;code&gt;FOREIGN KEY&lt;/code&gt; constraint immediately after each row. With two tables that reference each other and both columns &lt;code&gt;NOT NULL&lt;/code&gt;, no insertion order satisfies both checks at row-write time.&lt;/li&gt;
&lt;li&gt;The canonical fix is &lt;code&gt;DEFERRABLE INITIALLY IMMEDIATE&lt;/code&gt; on the constraint plus &lt;code&gt;SET CONSTRAINTS ALL DEFERRED&lt;/code&gt; inside the seed transaction. Postgres then validates the constraints at &lt;code&gt;COMMIT&lt;/code&gt; instead of after each row.&lt;/li&gt;
&lt;li&gt;If one side of the cycle is genuinely optional, a nullable back-edge plus a two-phase &lt;code&gt;INSERT … INSERT … UPDATE&lt;/code&gt; is simpler and doesn't change constraint semantics for normal app traffic.&lt;/li&gt;
&lt;li&gt;A single data-modifying CTE that inserts both sides with explicit, cross-referencing keys satisfies both foreign keys at the end of the statement — so it seeds the cycle even on the strict schema (&lt;code&gt;NOT NULL&lt;/code&gt; on both sides, no deferral) without a nullable column. The cost is that every key has to be known and hand-written up front.&lt;/li&gt;
&lt;li&gt;Before reaching for any workaround, ask whether the cycle is intrinsic to the domain or an accident of the schema. Removing a back-edge that loses business meaning is the first case; removing one that only changes how queries are written is the second, and refactoring beats &lt;code&gt;INITIALLY DEFERRED&lt;/code&gt; there.&lt;/li&gt;
&lt;li&gt;When the schema permits a two-phase insert, Seedfast handles it automatically — no hand-written SQL. When the schema doesn't (strict &lt;code&gt;NOT NULL&lt;/code&gt; on both sides with no deferral), no tool can; the deferred-constraint pattern in Workaround 1 stays the answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why a circular foreign key seed fails on the first INSERT
&lt;/h2&gt;

&lt;p&gt;Take the simplest concrete case the brief in your head is probably already running on. Two tables. Each one references the other. Both back-edges are &lt;code&gt;NOT NULL&lt;/code&gt; because the domain says they have to be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;primary_owner_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;full_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;organization_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;organizations_primary_owner_id_fkey&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primary_owner_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;employees_organization_id_fkey&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every employee belongs to an organization. Every organization has a primary owner who is one of its employees. Both halves are real, both halves are required, and the schema is fine — until the seed file runs.&lt;/p&gt;

&lt;p&gt;The naive seed reaches for the parent table first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_owner_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Northwind Logistics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Sam Patel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sam.patel@northwind.example'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;BEGIN
psql:/work/naive-insert.sql:9: ERROR: insert or update on table "organizations" violates foreign key constraint "organizations_primary_owner_id_fkey"
DETAIL: Key (primary_owner_id)=(1) is not present in table "employees".
psql:/work/naive-insert.sql:12: ERROR: current transaction is aborted, commands ignored until end of transaction block
ROLLBACK

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is one sentence: by default, Postgres runs each &lt;code&gt;FOREIGN KEY&lt;/code&gt; check at the moment a row is inserted, not at &lt;code&gt;COMMIT&lt;/code&gt;. The organization row needs an existing employee &lt;code&gt;1&lt;/code&gt;, and that row doesn't exist yet. Flipping the order — employee first — produces the symmetric error from &lt;code&gt;employees_organization_id_fkey&lt;/code&gt; because the organization the employee is supposed to belong to doesn't exist either. The cycle has no entry point under the default check timing. That timing is what each of the workarounds below changes, in different ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is your circular FK actually circular, or is it a schema smell?
&lt;/h2&gt;

&lt;p&gt;The competitor articles that rank for this query mostly skip this question and jump straight to &lt;code&gt;INITIALLY DEFERRED&lt;/code&gt;. It's worth pausing on, because the answer changes which workaround is the right one — or whether you need one at all.&lt;/p&gt;

&lt;p&gt;There's a useful distinction between &lt;strong&gt;intrinsic&lt;/strong&gt; cycles and &lt;strong&gt;accidental&lt;/strong&gt; ones.&lt;/p&gt;

&lt;p&gt;An intrinsic cycle is one where removing either back-edge changes the meaning of the data. The &lt;code&gt;organizations&lt;/code&gt; ↔ &lt;code&gt;employees&lt;/code&gt; cycle above is intrinsic: an organization without a designated primary owner is a different domain object, and an employee with no organization is too. Both directions of the relationship encode something the business actually cares about, and the cycle is the honest expression of that — it's also the case where &lt;a href="https://seedfa.st/blog/referential-integrity" rel="noopener noreferrer"&gt;what referential integrity actually guarantees&lt;/a&gt; starts to matter, because the cycle is the constraint, not just an ordering puzzle. For these, you have to pick a workaround — the cycle isn't going away.&lt;/p&gt;

&lt;p&gt;An accidental cycle is one where removing the back-edge only changes how queries are written. A common accidental shape is a &lt;code&gt;users.created_by_user_id&lt;/code&gt; self-reference plus a &lt;code&gt;created_users&lt;/code&gt; denormalized column on the parent — the same fact represented twice, in opposite directions, "for query performance". Another is a parent table with a &lt;code&gt;latest_&amp;lt;child&amp;gt;_id&lt;/code&gt; pointer — the child already references the parent, and the back-pointer exists so a join can be skipped. In both cases, the cycle is a cache, not a fact. The fix is to drop the cached column and write the join.&lt;/p&gt;

&lt;p&gt;A practical heuristic: if you remove the back-edge and the team has to re-explain a business rule (every organization must have a primary owner), the cycle is intrinsic. If you remove it and the team only has to rewrite one query, it's accidental, and the schema is the bug.&lt;/p&gt;

&lt;p&gt;For accidental cycles, the cheapest workaround is the migration that removes the cycle. A junction table dissolves the back-edge into a join. Moving one column to a third table — for example, an &lt;code&gt;organization_owners&lt;/code&gt; table with &lt;code&gt;organization_id&lt;/code&gt; and &lt;code&gt;employee_id&lt;/code&gt; — preserves the data without forcing either parent table to know about the other. These are migrations, not seed-script tricks, and they pay back every time anyone touches the schema afterwards.&lt;/p&gt;

&lt;p&gt;For intrinsic cycles, keep reading.&lt;/p&gt;

&lt;p&gt;In practice, cycles often arrive after the fact: a migration last sprint added a back-edge — a &lt;code&gt;created_by_user_id&lt;/code&gt; on &lt;code&gt;users&lt;/code&gt;, a &lt;code&gt;latest_invoice_id&lt;/code&gt; on &lt;code&gt;customers&lt;/code&gt; for a dashboard query — and the seed file that was fine yesterday now fails. This is a more common origin story than the schema looking circular from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workaround 1: deferred constraints inside the transaction
&lt;/h2&gt;

&lt;p&gt;The canonical Postgres answer is to tell the constraint to defer its check to &lt;code&gt;COMMIT&lt;/code&gt;. Two pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The constraints must be declared &lt;a href="https://www.postgresql.org/docs/current/sql-createtable.html" rel="noopener noreferrer"&gt;&lt;code&gt;DEFERRABLE&lt;/code&gt;&lt;/a&gt;. The default is &lt;code&gt;NOT DEFERRABLE&lt;/code&gt; and must be changed at the constraint level, either in the original &lt;code&gt;ALTER TABLE&lt;/code&gt; or in a follow-up migration.&lt;/li&gt;
&lt;li&gt;Inside the seed transaction, &lt;a href="https://www.postgresql.org/docs/current/sql-set-constraints.html" rel="noopener noreferrer"&gt;&lt;code&gt;SET CONSTRAINTS ALL DEFERRED&lt;/code&gt;&lt;/a&gt; flips the runtime behavior so checks run at &lt;code&gt;COMMIT&lt;/code&gt; instead of after each row.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your migrations live in an ORM (Prisma, Drizzle, TypeORM), this redeclaration is a raw-SQL migration step regardless — the &lt;code&gt;DEFERRABLE&lt;/code&gt; flag isn't expressible in their schema DSLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step A: redeclare the constraints as DEFERRABLE INITIALLY IMMEDIATE.&lt;/span&gt;
&lt;span class="c1"&gt;-- Default-immediate means normal app traffic still gets row-by-row checks.&lt;/span&gt;

&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt;
    &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;organizations_primary_owner_id_fkey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;organizations_primary_owner_id_fkey&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primary_owner_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;DEFERRABLE&lt;/span&gt; &lt;span class="k"&gt;INITIALLY&lt;/span&gt; &lt;span class="k"&gt;IMMEDIATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
    &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;employees_organization_id_fkey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;employees_organization_id_fkey&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;DEFERRABLE&lt;/span&gt; &lt;span class="k"&gt;INITIALLY&lt;/span&gt; &lt;span class="k"&gt;IMMEDIATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step B: the seed transaction itself.&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINTS&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;DEFERRED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_owner_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Northwind Logistics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Sam Patel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sam.patel@northwind.example'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_owner_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Cascadia Foods'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mira Okafor'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mira.okafor@cascadia.example'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run against the schema above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;BEGIN
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
COMMIT
BEGIN
SET CONSTRAINTS
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
COMMIT
  table_name | count
---------------+-------
 organizations | 2
 employees | 2
(2 rows)

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two trade-offs to know. First, deferred constraints cost more memory: Postgres has to track the pending checks until &lt;code&gt;COMMIT&lt;/code&gt;, and the bigger the transaction the higher the queue. For seed files that's usually fine; for million-row bulk loads it's worth measuring. &lt;a href="https://www.cybertec-postgresql.com/en/blog/" rel="noopener noreferrer"&gt;Cybertec's PostgreSQL blog&lt;/a&gt; makes the same point about the memory cost of holding deferred checks. Second, &lt;code&gt;INITIALLY IMMEDIATE&lt;/code&gt; is the safer default than &lt;code&gt;INITIALLY DEFERRED&lt;/code&gt;: it leaves normal application transactions on row-by-row checking and only relaxes that for sessions that explicitly opt in with &lt;code&gt;SET CONSTRAINTS ALL DEFERRED&lt;/code&gt;. The seed file opts in; the live app doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workaround 2: nullable foreign key + two-phase insert
&lt;/h2&gt;

&lt;p&gt;If your business rule actually allows one side of the cycle to be temporarily empty — for example, an organization can exist for a few rows of bookkeeping before its primary owner is decided — make that column &lt;code&gt;NULL&lt;/code&gt;-able and break the cycle in three statements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt;
    &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;primary_owner_id&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_owner_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Northwind Logistics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Cascadia Foods'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Sam Patel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sam.patel@northwind.example'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mira Okafor'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mira.okafor@cascadia.example'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;primary_owner_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;primary_owner_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-off is the obvious one: this requires the column to be nullable. If the domain says every organization must have a primary owner from the moment of its creation, you can't do this — the column has to stay &lt;code&gt;NOT NULL&lt;/code&gt;, and you're back to Workaround 1. Don't drop &lt;code&gt;NOT NULL&lt;/code&gt; purely to get the seed file to run, because that quietly weakens a business rule that the rest of the application relies on.&lt;/p&gt;

&lt;p&gt;Where this pattern earns its keep is when one side of the cycle is genuinely optional in the domain — a &lt;code&gt;manager_id&lt;/code&gt; that's nullable for the CEO's row, a &lt;code&gt;parent_category_id&lt;/code&gt; that's nullable for the root category. In those cases the schema already allows it, the seed file doesn't have to fight &lt;code&gt;DEFERRABLE&lt;/code&gt;, and the result reads like ordinary code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workaround 3: data-modifying CTE for one-shot inserts
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;NOT DEFERRABLE&lt;/code&gt; foreign key is checked at the end of each statement, not after each row — and a data-modifying &lt;a href="https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-MODIFYING" rel="noopener noreferrer"&gt;&lt;code&gt;WITH … RETURNING&lt;/code&gt;&lt;/a&gt; is one statement. So you can insert both sides of the cycle, each row naming the other's key, in a single statement: write the employees in a CTE, then the organizations in the outer &lt;code&gt;INSERT&lt;/code&gt;. By the time the statement ends and the foreign keys are checked, both rows exist, and both checks pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;new_employees&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Sam Patel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sam.patel@northwind.example'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
           &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mira Okafor'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mira.okafor@cascadia.example'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_owner_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Northwind Logistics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Cascadia Foods'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the one pattern here that runs on the strict schema unchanged — both back-edges &lt;code&gt;NOT NULL&lt;/code&gt;, neither constraint &lt;code&gt;DEFERRABLE&lt;/code&gt;, no &lt;code&gt;SET CONSTRAINTS&lt;/code&gt;. The order of the two INSERTs doesn't matter, and a dangling key still fails the end-of-statement check and rolls the whole statement back, so you don't trade away referential integrity to get the rows in.&lt;/p&gt;

&lt;p&gt;The catch is that every cross-referencing key has to be known and written by hand into the one statement. That's fine for a bounded set of reference rows; it doesn't scale to a seed file with hundreds of rows, and it can't lean on &lt;code&gt;BIGSERIAL&lt;/code&gt; — you can't reference an id the sequence hasn't handed out yet. It also doesn't help an automated seeder: a tool that inserts one table per statement never has both sides of the cycle in a single statement, so the end-of-statement trick isn't available to it. It's a sharp tool for a small, one-shot cyclic insert, written by hand.&lt;/p&gt;

&lt;p&gt;There's also a fourth pattern in the wild — &lt;code&gt;ALTER TABLE … DISABLE TRIGGER ALL&lt;/code&gt; to bypass FK checks during a sync. A widely-shared rubyrep gist recommends it for replication. Don't use it in a seed file. Disabling triggers lets bad data in and bypasses domain constraints, which is the failure mode the seed file is supposed to catch in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the seeder picks the order for you
&lt;/h2&gt;

&lt;p&gt;The patterns above pull two different levers. Deferred constraints and the two-phase insert change &lt;em&gt;when&lt;/em&gt; the check runs — deferred to &lt;code&gt;COMMIT&lt;/code&gt;, or split so the back-reference lands in a later &lt;code&gt;UPDATE&lt;/code&gt;. The single-statement CTE instead packs both sides into one statement, so the end-of-statement check already sees a closed cycle. The first two are the ones a seeder can apply for you: if the schema permits deferral or a nullable side, a tool that reads the schema can handle the cycle for you without you writing the SQL. The CTE stays hand-written, because it needs every key up front.&lt;/p&gt;

&lt;p&gt;That word "permits" is doing real work. Here's what happens when Seedfast runs against the strict-NOT-NULL schema we started with — both back-edges &lt;code&gt;NOT NULL&lt;/code&gt;, neither constraint declared &lt;code&gt;DEFERRABLE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2026-05-01T13:07:57+02:00] INFO: Seeding started
[2026-05-01T13:08:06+02:00] INFO: Tables in scope: public.organizations, public.employees
[2026-05-01T13:08:06+02:00] INFO: Planned: 8 records across 2 tables
[2026-05-01T13:08:06+02:00] INFO: Auto-approving plan (scope provided)
[2026-05-01T13:08:07+02:00] INFO: Seeding table public.organizations (1/2)
[2026-05-01T13:08:07+02:00] INFO: Seeding table public.employees (2/2)
[2026-05-01T13:09:31+02:00] ERROR: Failed table public.organizations: Failed: delta=0 &amp;lt; expected=2 (baseline=0, final=0)
[2026-05-01T13:09:31+02:00] ERROR: Failed table public.employees: Failed: delta=0 &amp;lt; expected=6 (baseline=0, final=0)
[2026-05-01T13:09:31+02:00] INFO: Seeding completed: 0/2 tables succeeded, 0 rows, 95.67s
[2026-05-01T13:09:31+02:00] WARN: 2 tables failed

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fails. So does every other seeder, because no tool can violate a &lt;code&gt;NOT NULL&lt;/code&gt; immediate-checked constraint that Postgres itself enforces row by row. Filling the tables is the seeder's job; constraint enforcement is the database's job, and that line doesn't move. A &lt;code&gt;DEFERRABLE INITIALLY IMMEDIATE&lt;/code&gt; variant behaves the same way unless the constraints are actually deferred at runtime — until they are, it's still a strict cycle, and declaring or deferring them is a schema decision the operator makes.&lt;/p&gt;

&lt;p&gt;Now drop &lt;code&gt;NOT NULL&lt;/code&gt; from &lt;code&gt;organizations.primary_owner_id&lt;/code&gt; — the nullable variant we used in Workaround 2 — and run the same scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2026-05-01T13:11:58+02:00] INFO: Seeding started
[2026-05-01T13:12:03+02:00] INFO: Tables in scope: public.organizations, public.employees
[2026-05-01T13:12:03+02:00] INFO: Planned: 8 records across 2 tables
[2026-05-01T13:12:03+02:00] INFO: Auto-approving plan (scope provided)
[2026-05-01T13:12:04+02:00] INFO: Seeding table public.organizations (1/2)
[2026-05-01T13:12:04+02:00] INFO: Seeding table public.employees (2/2)
[2026-05-01T13:12:32+02:00] INFO: Table public.organizations completed: 2 rows in 27.144s
[2026-05-01T13:12:32+02:00] INFO: Table public.employees completed: 6 rows in 27.144s
[2026-05-01T13:12:32+02:00] INFO: Seeding completed: 2/2 tables succeeded, 8 rows, 35.16s

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two organizations, six employees, in cyclic order, with each organization's primary owner one of its own employees — and no hand-written two-phase INSERT. When the schema gives Seedfast room — one side nullable, or &lt;code&gt;DEFERRABLE INITIALLY DEFERRED&lt;/code&gt; declared and accepted at runtime — Seedfast handles it automatically, and the operator writes zero seed-side SQL. The other case — strict &lt;code&gt;NOT NULL&lt;/code&gt; on both sides with no deferral — no tool can solve in software; that's a schema decision, not a tooling one.&lt;/p&gt;

&lt;p&gt;If your seed file keeps running into this and your schema permits the two-phase write, see &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt; for what that looks like end-to-end on your own database. If it doesn't permit it, the deferred-constraint pattern in Workaround 1 is still the cleanest answer, and it's worth rolling once into the migration that adds the back-edge so the next person to write a seed file doesn't repeat this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do you seed two Postgres tables with circular foreign keys?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Declare both foreign keys &lt;code&gt;DEFERRABLE INITIALLY IMMEDIATE&lt;/code&gt;, then wrap the seed inserts in a transaction that runs &lt;code&gt;SET CONSTRAINTS ALL DEFERRED&lt;/code&gt;.&lt;/strong&gt; Postgres validates both constraints at &lt;code&gt;COMMIT&lt;/code&gt; instead of after each row, so the two mutually-referencing rows can be inserted in either order inside one transaction without tripping the foreign-key check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does every insert order fail with a circular foreign key?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;By default Postgres checks each &lt;code&gt;FOREIGN KEY&lt;/code&gt; the moment a row is written, not at &lt;code&gt;COMMIT&lt;/code&gt;.&lt;/strong&gt; When two tables reference each other and both columns are &lt;code&gt;NOT NULL&lt;/code&gt;, the first row you insert always references a parent that doesn't exist yet — and flipping the order produces the symmetric error from the other constraint. The cycle has no valid entry point under immediate checking.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does &lt;code&gt;SET CONSTRAINTS ALL DEFERRED&lt;/code&gt; do?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;It postpones every deferrable constraint check in the current transaction until &lt;code&gt;COMMIT&lt;/code&gt;.&lt;/strong&gt; Only constraints already declared &lt;code&gt;DEFERRABLE&lt;/code&gt; are affected; non-deferrable ones still fire row by row. The live application keeps immediate checking unless it opts in, so the relaxed timing stays scoped to the seed transaction that issued the statement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can you seed a circular foreign key without making a column nullable?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Yes — two patterns do.&lt;/strong&gt; Deferred constraints keep both columns &lt;code&gt;NOT NULL&lt;/code&gt; and validate at &lt;code&gt;COMMIT&lt;/code&gt;. A single data-modifying CTE that inserts both sides with explicit cross-referencing keys also keeps them &lt;code&gt;NOT NULL&lt;/code&gt;, because a non-deferrable foreign key is checked at end-of-statement and a writable CTE is one statement. Only the two-phase &lt;code&gt;INSERT … INSERT … UPDATE&lt;/code&gt; needs a nullable back-reference column.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does &lt;code&gt;DEFERRABLE INITIALLY IMMEDIATE&lt;/code&gt; slow down normal application traffic?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;No — &lt;code&gt;INITIALLY IMMEDIATE&lt;/code&gt; leaves everyday transactions on row-by-row checking.&lt;/strong&gt; Only a session that explicitly runs &lt;code&gt;SET CONSTRAINTS ALL DEFERRED&lt;/code&gt; relaxes the timing. Deferred checks do cost extra memory, because Postgres queues the pending checks until &lt;code&gt;COMMIT&lt;/code&gt; — that matters for million-row bulk loads, but rarely for a seed file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/referential-integrity" rel="noopener noreferrer"&gt;What is Referential Integrity?&lt;/a&gt; — the property the cycle is bumping into, with the self-referential and composite-key edge cases that share the same insert-order discipline&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/postgres-seed-script" rel="noopener noreferrer"&gt;Postgres Seed Script: Survive the Next Migration&lt;/a&gt; — what to do once you have working SQL: idempotency, sequence resets, and writing a seed script that doesn't break the next time the schema moves&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Get started with Seedfast&lt;/a&gt; — connect to your PostgreSQL database and run a seed against your own schema in under five minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/circular-foreign-key-seed" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>database</category>
      <category>programming</category>
    </item>
    <item>
      <title>One docker compose up, One Seeded Postgres</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:31:58 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/one-docker-compose-up-one-seeded-postgres-14jh</link>
      <guid>https://dev.to/mikh-shytsko/one-docker-compose-up-one-seeded-postgres-14jh</guid>
      <description>&lt;p&gt;Docker Compose made your dev database reproducible, one command bringing up the same Postgres for the whole team, down to the version and extensions, and the schema it hands you arrives perfect and empty. That emptiness is the part nobody standardized, so &lt;code&gt;docker compose up&lt;/code&gt; gets you a database while filling it remains a separate step every project reinvents.&lt;/p&gt;

&lt;p&gt;A docker compose seed database workflow comes down to two decisions, where the seed command runs and how it knows Postgres is ready for it. Get both right and the empty-database step folds into &lt;code&gt;docker compose up&lt;/code&gt;. The rest of the page applies &lt;a href="https://seedfa.st/blog/database-seeding" rel="noopener noreferrer"&gt;database seeding&lt;/a&gt; to this one stack, using the compose file I run most days.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Seedfast seeds a Postgres running under Docker Compose in one command. Bring the stack up with &lt;code&gt;docker compose up -d --wait&lt;/code&gt; so the &lt;code&gt;pg_isready&lt;/code&gt; healthcheck gates readiness, then run &lt;code&gt;seedfast seed&lt;/code&gt; from the host against the published port. Keep schema bootstrap in &lt;code&gt;/docker-entrypoint-initdb.d&lt;/code&gt;, and let the healthcheck-gated seed step handle the test data you actually iterate on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where the seed goes in a docker compose seed database setup
&lt;/h2&gt;

&lt;p&gt;Three spots can hold the seed, and they aren't interchangeable. The first is &lt;code&gt;/docker-entrypoint-initdb.d&lt;/code&gt;, the postgres image's init directory, where scripts run exactly once, on the first boot of an empty data directory. Inside the compose file itself sits the second, a one-shot service that waits for Postgres to report healthy, runs the seed, and exits — and the third stays outside compose altogether, a seed run from your host shell once the healthcheck clears.&lt;/p&gt;

&lt;p&gt;Which one fits turns on how often the data changes. Schema bootstrap and a &lt;code&gt;CREATE EXTENSION&lt;/code&gt; line belong at first boot, where they run once and stay put, while test data you regenerate as the schema moves wants a health-gated placement, one that re-runs on demand instead of hiding behind a volume populated weeks ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  The compose file that boots a ready Postgres
&lt;/h2&gt;

&lt;p&gt;Here is the compose file I start from. It defines one postgres service on the official &lt;code&gt;postgres:16&lt;/code&gt; image, publishes the port to the host, and keeps its data in a named volume so a restart doesn't wipe your work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of everything in that file, the healthcheck carries the most weight. Docker reports a container "running" the instant its process starts, but PostgreSQL needs a moment to finish recovery and open its socket, and a seed that fires inside that window dies on "connection refused" — the gap &lt;code&gt;pg_isready&lt;/code&gt; closes, since it exits 0 only once the server is accepting connections. Binding the container's health to that check makes "healthy" and "ready to seed" the same event, and the &lt;code&gt;interval&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;retries&lt;/code&gt;, and &lt;code&gt;start_period&lt;/code&gt; fields tune how long Docker probes before it declares failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What /docker-entrypoint-initdb.d actually does
&lt;/h2&gt;

&lt;p&gt;The init directory is where most people meet this pattern, and where they hit its sharp edge. On first boot, and only while the data directory is still empty, the postgres image runs everything in &lt;code&gt;/docker-entrypoint-initdb.d&lt;/code&gt; in alphabetical order (every &lt;code&gt;*.sql&lt;/code&gt;, &lt;code&gt;*.sql.gz&lt;/code&gt;, and &lt;code&gt;*.sh&lt;/code&gt; file). Mount &lt;code&gt;01-schema.sql&lt;/code&gt; and &lt;code&gt;02-seed.sql&lt;/code&gt; there and a fresh volume picks them up.&lt;/p&gt;

&lt;p&gt;The trap is the word "fresh." Because the named volume persists across &lt;code&gt;docker compose down&lt;/code&gt; and every ordinary &lt;code&gt;docker compose up&lt;/code&gt;, the init scripts never fire again. Edit &lt;code&gt;02-seed.sql&lt;/code&gt;, run &lt;code&gt;docker compose up&lt;/code&gt; again, and nothing changes, because the data directory isn't empty anymore and the image skips the whole directory. Only &lt;code&gt;docker compose down -v&lt;/code&gt; drops the volume and lets the next &lt;code&gt;up&lt;/code&gt; start clean, which makes the init directory a good home for schema bootstrap and a &lt;code&gt;CREATE EXTENSION pgcrypto&lt;/code&gt; line, and a poor one for the test rows you tweak between runs. For the mechanics of the &lt;code&gt;seed.sql&lt;/code&gt; those scripts contain, &lt;a href="https://seedfa.st/blog/postgres-seed-script" rel="noopener noreferrer"&gt;writing a Postgres seed script&lt;/a&gt; covers the file; the point here is only where it belongs in a compose stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The healthcheck-gated seed step
&lt;/h2&gt;

&lt;p&gt;By default I reach for this placement — bring the stack up and wait for health:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--wait&lt;/code&gt; holds until every healthcheck reports healthy, then hands the shell back — no &lt;code&gt;sleep 10&lt;/code&gt; guesswork, no polling loop. Once it returns, Postgres is ready, and the seed runs against the published port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SEEDFAST_DSN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql://app:app@localhost:5432/app &lt;span class="se"&gt;\&lt;/span&gt;
  npx seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"customers with a few orders and line items each"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; plain

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That connection string is the same &lt;code&gt;localhost:5432&lt;/code&gt; you would hand to &lt;code&gt;psql&lt;/code&gt;, reaching the container through the published port. &lt;code&gt;npx&lt;/code&gt; pulls the CLI from npm; &lt;code&gt;SEEDFAST_DSN&lt;/code&gt; tells the seed where to write and takes priority over &lt;code&gt;DATABASE_URL&lt;/code&gt;, so it won't clobber another tool's setting; &lt;code&gt;SEEDFAST_API_KEY&lt;/code&gt;, exported in your shell beforehand, authenticates the run; and &lt;code&gt;--scope&lt;/code&gt; feeds Seedfast a plain-English description and skips the interactive prompt that would otherwise stall a script. Under the hood it reads the live schema and writes valid, connected rows, so every foreign key resolves to a parent that already exists. The flags match the &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;CLI quick start&lt;/a&gt;; nothing changes because Postgres lives in a container.&lt;/p&gt;

&lt;p&gt;If you would rather keep everything in one file, lift that command into a one-shot service that waits on Postgres:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;seed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:22&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;SEEDFAST_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SEEDFAST_API_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;SEEDFAST_DSN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://app:app@postgres:5432/app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx -y seedfast seed --scope "customers with a few orders each" --output plain&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;condition: service_healthy&lt;/code&gt; is the piece that matters, holding the &lt;code&gt;seed&lt;/code&gt; container until the healthcheck passes so the seed never races a database that hasn't opened its socket. Inside the compose network the host becomes &lt;code&gt;postgres&lt;/code&gt;, the service name, since &lt;code&gt;localhost&lt;/code&gt; points the container at itself.&lt;/p&gt;

&lt;p&gt;The loop stays short when the schema moves. After a migration adds a column, drop the volume and bring the stack back up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then rerun the same &lt;code&gt;seedfast seed&lt;/code&gt; command, and the new column simply appears in the next batch of generated rows, because the schema read happens fresh on every run rather than being cached from the first one.&lt;/p&gt;

&lt;p&gt;None of this is only a local trick; the &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD database seeding&lt;/a&gt; guide runs the same gated-seed shape against a &lt;a href="https://seedfa.st/blog/github-actions-seed-postgres-database" rel="noopener noreferrer"&gt;GitHub Actions service container&lt;/a&gt;, on the identical healthcheck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why doesn't my seed script in docker-entrypoint-initdb.d run again?
&lt;/h3&gt;

&lt;p&gt;Scripts in &lt;code&gt;/docker-entrypoint-initdb.d&lt;/code&gt; run only on the first boot of an empty data directory, so a persisted named volume is almost always the reason a second run does nothing. The image skips the entire init directory whenever its data directory already holds a database, and no edit to a seed script there takes effect until the volume is gone — run &lt;code&gt;docker compose down -v&lt;/code&gt; to drop it, bring the stack back up, and the scripts execute against a clean directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Seedfast seed a Postgres database running in Docker Compose?
&lt;/h3&gt;

&lt;p&gt;Seedfast treats a Compose-managed Postgres like any other database, reaching it over the port the compose file publishes. Point &lt;code&gt;SEEDFAST_DSN&lt;/code&gt; at &lt;code&gt;postgresql://user:pass@localhost:5432/db&lt;/code&gt; once &lt;code&gt;docker compose up -d --wait&lt;/code&gt; returns, and run &lt;code&gt;seedfast seed&lt;/code&gt;. Migrations don't break it, since the schema is read live on every run, and when you would sooner keep everything inside the file, the same command drops into a one-shot Compose service.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make Docker Compose wait for Postgres before seeding?
&lt;/h3&gt;

&lt;p&gt;Docker Compose waits for Postgres when the service carries a &lt;code&gt;pg_isready&lt;/code&gt; healthcheck and you gate the seed on it. Either bring the stack up with &lt;code&gt;docker compose up -d --wait&lt;/code&gt;, which returns only after every healthcheck passes, or give the seed service a &lt;code&gt;depends_on&lt;/code&gt; block with &lt;code&gt;condition: service_healthy&lt;/code&gt; so Compose holds it until the database is accepting connections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should test data be baked into the Docker image or seeded after startup?
&lt;/h3&gt;

&lt;p&gt;Test data you change with any regularity belongs in a seed step after startup, well away from the image build and the first-boot init script. A baked-in dataset goes stale the moment a migration lands and forces an image rebuild to refresh, whereas a post-startup seed reads the current schema and regenerates rows on demand, which is why the image and init directory should keep only the parts that rarely move, the schema and required extensions. A health-gated seed handles the data your tests assert against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seed the Postgres your stack boots empty
&lt;/h2&gt;

&lt;p&gt;The compose file above hands every machine on your team the same clean Postgres, and &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; fills it with referentially valid rows generated from the live schema, no production data anywhere in the run. Start on the &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;free plan&lt;/a&gt; without a card, and the whole loop, &lt;code&gt;up --wait&lt;/code&gt; included, fits inside a few minutes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/docker-compose-seed-database" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>postgres</category>
      <category>devops</category>
      <category>database</category>
    </item>
    <item>
      <title>Prisma Postgres Seed: prisma db seed, psql, and the Direct URL</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:31:09 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/prisma-postgres-seed-prisma-db-seed-psql-and-the-direct-url-2ab5</link>
      <guid>https://dev.to/mikh-shytsko/prisma-postgres-seed-prisma-db-seed-psql-and-the-direct-url-2ab5</guid>
      <description>&lt;p&gt;&lt;em&gt;Prisma Postgres seed in one command — or rewrite your seed script every time the schema changes. Prisma's managed Postgres spins up an empty database in seconds; then you have to fill it. Here are three ways to do it without fighting the connection string.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; To seed a Prisma Postgres database, run &lt;code&gt;npx prisma db seed&lt;/code&gt; against the &lt;strong&gt;direct&lt;/strong&gt; connection string (&lt;code&gt;db.prisma.io&lt;/code&gt;, not &lt;code&gt;pooled.db.prisma.io&lt;/code&gt;). Configure &lt;code&gt;seed: "tsx prisma/seed.ts"&lt;/code&gt; in &lt;code&gt;prisma.config.ts&lt;/code&gt; and point &lt;code&gt;directUrl&lt;/code&gt; at the direct URL in &lt;code&gt;schema.prisma&lt;/code&gt;. In Prisma ORM v7, &lt;code&gt;migrate dev&lt;/code&gt; no longer triggers the seed — invoke &lt;code&gt;prisma db seed&lt;/code&gt; explicitly. For schemas past ~15 related tables, &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; reads the live schema on each run and skips the seed file entirely.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prisma Postgres ships with &lt;strong&gt;two TCP connection strings&lt;/strong&gt; — direct (&lt;code&gt;db.prisma.io:5432&lt;/code&gt;) and pooled (&lt;code&gt;pooled.db.prisma.io:5432&lt;/code&gt;). Always seed against the &lt;strong&gt;direct&lt;/strong&gt; URL. The pooled endpoint runs through a connection pooler and breaks long transactions and prepared statements, the same class of failure Neon and Supabase users hit on their poolers&lt;/li&gt;
&lt;li&gt;For edge runtimes that can't open a TCP socket (Cloudflare Workers, Vercel Edge), the &lt;code&gt;@prisma/adapter-ppg&lt;/code&gt; package — exporting &lt;code&gt;PrismaPostgresAdapter&lt;/code&gt; — wraps Prisma's serverless driver and accepts the same direct TCP connection string. For Node-side seeds the plain TCP URL with the standard &lt;code&gt;pg&lt;/code&gt; driver is simpler&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prisma db seed&lt;/code&gt; still works — but in Prisma ORM v7 it no longer runs automatically during &lt;code&gt;prisma migrate dev&lt;/code&gt;. You have to invoke it explicitly, or call it from your CI step&lt;/li&gt;
&lt;li&gt;Prisma Postgres has no production data to copy, no PII, no compliance sign-off — just an empty schema waiting for realistic test data. That's the same starting point developers in fintech, healthcare, and other regulated industries hit on every greenfield project&lt;/li&gt;
&lt;li&gt;When the schema changes — which it will — hand-written seed files break. &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; reads the live schema on every run and regenerates valid, connected data, so there's no &lt;code&gt;seed.ts&lt;/code&gt; to maintain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quick fix if you landed here from a broken seed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Pooled URL — breaks seeding (transaction-mode pooler, no prepared statements)
postgres://USER:PASS@pooled.db.prisma.io:5432/?sslmode=require

# Direct URL — use this for seeds, migrations, admin scripts
postgres://USER:PASS@db.prisma.io:5432/?sslmode=require

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of this guide walks through &lt;code&gt;psql&lt;/code&gt;, &lt;code&gt;prisma db seed&lt;/code&gt;, and the &lt;code&gt;@prisma/adapter-ppg&lt;/code&gt; serverless-driver path that actually work against Prisma Postgres, and shows how to keep the dataset alive across migrations.&lt;/p&gt;

&lt;p&gt;You provisioned Prisma Postgres because it spins up instantly, scales to zero, and ships with the tooling you already use — &lt;code&gt;prisma migrate&lt;/code&gt;, the Prisma Client, the new &lt;code&gt;create-prisma&lt;/code&gt; scaffold. But the database is empty, and the only realistic data you can copy in from somewhere is production data — which, if you work in a regulated industry, you're not allowed to touch. To seed a Prisma Postgres database, you need three things: the right connection string, a seed strategy that survives schema changes, and a way to make the data look like the real thing without ever leaving development. This guide covers all three.&lt;/p&gt;

&lt;p&gt;If you want the framework-agnostic version of this article first, &lt;a href="https://seedfa.st/blog/seed-database" rel="noopener noreferrer"&gt;how to seed a database&lt;/a&gt; covers the cross-stack fundamentals. This article is specifically about Prisma Postgres — the managed product, not the ORM seed pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Prisma Postgres database needs seeding
&lt;/h2&gt;

&lt;p&gt;A fresh Prisma Postgres database is empty. Migrations create the schema; nothing fills it. Unlike a long-lived shared dev database that accumulates data over months, a Prisma Postgres project is born clean and stays clean until you put something in it.&lt;/p&gt;

&lt;p&gt;Three scenarios force the seeding question:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;New project onboarding.&lt;/strong&gt; A teammate clones the repo, runs &lt;code&gt;npx prisma migrate dev&lt;/code&gt;, and ends up with thirty empty tables. The app boots but every list view is blank.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI and ephemeral environments.&lt;/strong&gt; Each CI run, each preview deploy, each rebuilt local stack starts from zero. Without a seed step, end-to-end tests hit empty queries and fail in ways that have nothing to do with the change being tested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demos and staging.&lt;/strong&gt; You need 500 products, a year of order history, and users that look real. Faker-style random columns aren't good enough — the data has to hold up under a 10-minute customer demo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Prisma's own docs cover the &lt;strong&gt;mechanics&lt;/strong&gt; of &lt;code&gt;prisma db seed&lt;/code&gt; — how to wire up &lt;code&gt;prisma.config.ts&lt;/code&gt;, how to run TypeScript seeds with &lt;code&gt;tsx&lt;/code&gt;. They don't cover the &lt;strong&gt;lifecycle problem&lt;/strong&gt; : the seed file gets stale on every migration, and the team that wrote it hasn't touched it in six weeks. That's the part that bites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prisma Postgres seed connection strings: direct vs pooled
&lt;/h2&gt;

&lt;p&gt;Every Prisma Postgres database exposes two TCP connection strings. The dashboard shows both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Direct (one-to-one, full Postgres protocol, prepared statements work)
postgres://USER:PASS@db.prisma.io:5432/?sslmode=require

# Pooled (transaction-mode pooler, optimized for short-lived requests)
postgres://USER:PASS@pooled.db.prisma.io:5432/?sslmode=require

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is the &lt;code&gt;pooled.&lt;/code&gt; subdomain. The pooled endpoint sits behind a transaction-mode connection pooler — every query gets a fresh backend connection, which is great for serverless apps with bursty traffic, and bad for everything else. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prepared statements don't survive across transactions, so any client that prepares a query (most of them) will hit &lt;code&gt;prepared statement "s1" already exists&lt;/code&gt; after the first reuse&lt;/li&gt;
&lt;li&gt;Long-running operations like a multi-statement seed transaction can be terminated when the pool decides to recycle the backend&lt;/li&gt;
&lt;li&gt;Session-level settings (&lt;code&gt;SET&lt;/code&gt;, advisory locks, temporary tables that span statements) silently disappear&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For seeding, migrations, and any admin script, use the &lt;strong&gt;direct&lt;/strong&gt; URL. Wire it up explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env.local
&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;=&lt;span class="s2"&gt;"postgres://USER:PASS@pooled.db.prisma.io:5432/?sslmode=require"&lt;/span&gt; &lt;span class="c"&gt;# app runtime
&lt;/span&gt;&lt;span class="n"&gt;DIRECT_URL&lt;/span&gt;=&lt;span class="s2"&gt;"postgres://USER:PASS@db.prisma.io:5432/?sslmode=require"&lt;/span&gt; &lt;span class="c"&gt;# migrations + seeds
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;schema.prisma&lt;/code&gt;, point &lt;code&gt;directUrl&lt;/code&gt; at the direct URL — Prisma uses it for migrations automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;datasource&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"postgresql"&lt;/span&gt;
  &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;directUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"DIRECT_URL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a multi-statement seed dies halfway through with &lt;code&gt;prepared statement "s1" already exists&lt;/code&gt; or &lt;code&gt;cached plan must not change result type&lt;/code&gt;, the script is talking to the pooler. Point it at the direct URL and re-run.&lt;/p&gt;

&lt;p&gt;You may also see the older Prisma Accelerate–style URL — &lt;code&gt;prisma+postgres://accelerate.prisma-data.net/?api_key=...&lt;/code&gt; — referenced in some examples. That URL is part of the Accelerate caching layer, not the standard Prisma Postgres connection. The two TCP strings above are what the Prisma Console gives you for a Prisma Postgres database, and they cover every seeding scenario this guide describes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: Prisma Postgres seed with raw SQL
&lt;/h2&gt;

&lt;p&gt;The simplest path. Write &lt;code&gt;INSERT&lt;/code&gt;s, run them with &lt;code&gt;psql&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- seed.sql&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'acme-corp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'team'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'beta-labs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'free'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;NOTHING&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mobile rewrite'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Billing refactor'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'planned'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Internal demo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;NOTHING&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="n"&gt;psql&lt;/span&gt; &lt;span class="nv"&gt;"$DIRECT_URL"&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SSL is mandatory on Prisma Postgres — the &lt;code&gt;sslmode=require&lt;/code&gt; parameter on the connection string handles that automatically. Drop it and you'll get &lt;code&gt;connection requires SSL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For larger reference loads, &lt;code&gt;COPY FROM STDIN&lt;/code&gt; is meaningfully faster than row-by-row inserts because it skips per-row parsing and planning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIRECT_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"COPY products (name, price, category_id) FROM STDIN CSV"&lt;/span&gt; &amp;lt; products.csv

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt; keeps the seed idempotent — CI can run it twice without falling over on duplicate keys. For values that should always reflect the latest state, use &lt;code&gt;ON CONFLICT DO UPDATE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;feature_flags&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'new_checkout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Raw SQL is fine for &lt;strong&gt;reference data&lt;/strong&gt; — feature flags, country codes, role definitions, the dozen rows your app reads at boot. It starts breaking once you have ten or fifteen tables with foreign keys, because every migration that adds a &lt;code&gt;NOT NULL&lt;/code&gt; column or a new FK forces you to hand-edit the seed. &lt;a href="https://seedfa.st/blog/seed-file-maintenance" rel="noopener noreferrer"&gt;Seed file maintenance&lt;/a&gt; covers why this lifecycle is so brutal on active codebases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: Seed Prisma Postgres with prisma db seed
&lt;/h2&gt;

&lt;p&gt;Most teams using Prisma Postgres are also using Prisma ORM, so the natural path is &lt;code&gt;prisma db seed&lt;/code&gt;. In current Prisma versions, the seed command is configured in &lt;code&gt;prisma.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// prisma.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prisma/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prisma/schema.prisma&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;migrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prisma/migrations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tsx prisma/seed.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;datasource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DIRECT_URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typical TypeScript seed against Prisma Postgres looks like any other Prisma seed — point the client at the direct URL and use the standard API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// prisma/seed.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;org&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acme-corp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acme-corp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;team&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;organizationId_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mobile rewrite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mobile rewrite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$disconnect&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;


&lt;span class="nx"&gt;npx&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things changed in Prisma ORM v7 that catch teams off guard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;prisma migrate dev&lt;/code&gt; no longer runs &lt;code&gt;prisma generate&lt;/code&gt; or the seed automatically. The &lt;code&gt;--skip-seed&lt;/code&gt; flag is gone because there's nothing to skip — you have to invoke &lt;code&gt;prisma db seed&lt;/code&gt; explicitly when you want it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prisma migrate reset&lt;/code&gt; still triggers the seed, so the "wipe and restart" workflow is intact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the seed to run after every migration locally, wrap it in a &lt;code&gt;package.json&lt;/code&gt; script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"db:reset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prisma migrate reset --force"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"db:migrate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prisma migrate dev &amp;amp;&amp;amp; prisma db seed"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Seeding from an edge runtime with &lt;code&gt;@prisma/adapter-ppg&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Most seeds run in Node — locally or in CI — and the plain TCP direct URL is the right choice there. If the seed has to run from an environment that can't open a TCP socket (a Cloudflare Worker, a Vercel Edge function, a managed worker without raw networking), use Prisma's serverless driver adapter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;prisma&lt;/span&gt;&lt;span class="sr"&gt;/adapter-pp&lt;/span&gt;&lt;span class="err"&gt;g
&lt;/span&gt;

&lt;span class="c1"&gt;// prisma/seed.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaPostgresAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/adapter-ppg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaPostgresAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DIRECT_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acme-corp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acme-corp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;team&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$disconnect&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter takes the same direct TCP connection string and routes queries over Prisma's HTTP/WebSocket transport, so the seed runs anywhere the Prisma Client runs — but for a regular Node script, plain TCP without the adapter is one fewer dependency.&lt;/p&gt;

&lt;p&gt;The bigger problem with both &lt;code&gt;prisma db seed&lt;/code&gt; paths is the same one raw SQL has: the values are hand-written. When a migration adds &lt;code&gt;accounts.organization_id NOT NULL&lt;/code&gt;, the seed breaks on the next run. Someone — usually whoever picked up the on-call rotation — has to fix it before the rest of the team can boot the app.&lt;/p&gt;

&lt;p&gt;If that lifecycle sounds like every Monday morning on your team, &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; skips the seed file entirely — generating the dataset from the live schema on every run. The next section covers how it fits into a Prisma Postgres workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: Schema-aware seeding with Seedfast
&lt;/h2&gt;

&lt;p&gt;Point Seedfast at the Prisma Postgres direct URL, describe the dataset in plain English, and Seedfast reads the schema — tables, columns, constraints, foreign keys — and generates a valid, connected dataset that fits it. There's no &lt;code&gt;seed.sql&lt;/code&gt; or &lt;code&gt;seed.ts&lt;/code&gt; in the repo, because the schema is the seed plan.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; seedfast
&lt;span class="c"&gt;# or: brew install argon-it/tap/seedfast&lt;/span&gt;

&lt;span class="c"&gt;# Log in and connect — paste the direct (db.prisma.io) connection string&lt;/span&gt;
seedfast connect

&lt;span class="c"&gt;# Generate data from the current schema&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"two organizations on the team plan, 8 active projects, varied task assignments"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next migration adds a column? Seedfast reads it on the following run and generates the new field. The next migration adds a whole table? Same — picked up on the next run. Every row comes out valid and connected — including self-referencing tables and tables that reference each other — so there's no hand-written FK list to maintain.&lt;/p&gt;

&lt;p&gt;The scope changes per environment, the command doesn't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Local dev loop&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"2 organizations, 4 users each, 5 projects between them"&lt;/span&gt;

&lt;span class="c"&gt;# Preview branch for a PR that touches the billing flow&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"1 organization mid-trial with overdue invoice and 3 retried payments"&lt;/span&gt;

&lt;span class="c"&gt;# Sales demo&lt;/span&gt;
seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"500 organizations across 3 plan tiers, 6 months of project history"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;prisma migrate deploy&lt;/code&gt; first to make sure the database is on the latest schema, then run Seedfast against the same direct URL. Prisma owns the structure; Seedfast fills it.&lt;/p&gt;

&lt;p&gt;For production reference data — feature flags, country codes, admin roles — keep the versioned SQL or the Prisma seed. That data belongs to the application and should ship through migrations. Seedfast targets the larger, evolving development, CI, staging, and demo datasets that go stale every time the schema moves — not the dozen rows that go to prod.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters in regulated industries
&lt;/h3&gt;

&lt;p&gt;For teams that &lt;strong&gt;can't&lt;/strong&gt; copy production data into dev — fintech, healthcare, anyone under HIPAA, GDPR, or SOC 2 review — Seedfast removes the blocker that hand-written seeds and anonymization pipelines both leave in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seedfast generates from the schema, not a snapshot.&lt;/strong&gt; No production access, no PII pipeline, no anonymization step. Schema metadata (table and column shapes) is sent over the wire to Seedfast's generation service so the model knows what to build; row values are not. The result is realistic test data without ever pulling production rows out of their environment. For the wider workflow, see &lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;staging without production data&lt;/a&gt; and the &lt;a href="https://seedfa.st/blog/hipaa-test-data" rel="noopener noreferrer"&gt;HIPAA test data&lt;/a&gt; playbook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seedfast fills a database, not a column.&lt;/strong&gt; Faker hands back random strings. Seedfast generates connected organizations, accounts, transactions, and audit rows with valid foreign keys, distributions that look like a real product, and values that pass a domain check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seedfast scales with one command.&lt;/strong&gt; The same &lt;code&gt;seedfast seed&lt;/code&gt; runs ten rows for a unit test and half a million for a load test — only the &lt;code&gt;--scope&lt;/code&gt; changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The free plan covers small dev datasets with no credit card, so a Friday spike costs nothing — &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;connect and run your first seed&lt;/a&gt; in about two minutes, or check &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; before sending it to your CTO.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Prisma Postgres seeding issues and how to fix them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;prepared statement "s1" already exists&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;You're seeding through the pooled URL (&lt;code&gt;pooled.db.prisma.io&lt;/code&gt;). The transaction-mode pooler discards prepared statements between transactions, and the driver tries to reuse one that's gone. Switch to the direct URL for the seed.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;cached plan must not change result type&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A server-side Postgres plan cache error: a prepared statement's result type changed underneath it, usually because a migration ran between two seed invocations on the same connection. Run migrations to completion before the seed, and use the direct URL — the pooler can mask this error in confusing ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;connection requires SSL&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Your connection string is missing &lt;code&gt;sslmode=require&lt;/code&gt;. Prisma Postgres rejects non-SSL connections. Add the parameter, or set &lt;code&gt;PGSSLMODE=require&lt;/code&gt; in the environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;the URL must start with the protocol postgresql:// or postgres://&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;prisma+postgres://...&lt;/code&gt; URL slipped into &lt;code&gt;DIRECT_URL&lt;/code&gt;. The Prisma Console issues TCP strings (&lt;code&gt;db.prisma.io&lt;/code&gt; / &lt;code&gt;pooled.db.prisma.io&lt;/code&gt;) for Prisma Postgres — use those for &lt;code&gt;psql&lt;/code&gt;, the &lt;code&gt;pg&lt;/code&gt; driver, and &lt;code&gt;prisma db seed&lt;/code&gt; without an adapter. The Prisma-protocol URL is not part of the standard seeding path.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;prisma db seed&lt;/code&gt; does nothing during &lt;code&gt;prisma migrate dev&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the Prisma ORM v7 behavior change. &lt;code&gt;migrate dev&lt;/code&gt; no longer auto-seeds. Either invoke &lt;code&gt;prisma db seed&lt;/code&gt; explicitly, or chain it in a script: &lt;code&gt;prisma migrate dev &amp;amp;&amp;amp; prisma db seed&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;relation "users" does not exist&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The seed ran before the migration. The order is always &lt;strong&gt;migrate then seed&lt;/strong&gt; , never the reverse. In CI, make &lt;code&gt;prisma migrate deploy&lt;/code&gt; (or &lt;code&gt;prisma migrate reset --force&lt;/code&gt; on ephemeral branches) a hard prerequisite of the seed step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Seed succeeds locally, fails in CI
&lt;/h3&gt;

&lt;p&gt;Two recurring causes: CI uses the pooled URL because that's what the app uses (switch to the direct URL for the seed step), or the CI environment hasn't run migrations against the freshly-created database (run &lt;code&gt;prisma migrate deploy&lt;/code&gt; before the seed).&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual SQL vs prisma db seed vs Seedfast
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Raw SQL (&lt;code&gt;psql -f&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;&lt;code&gt;prisma db seed&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Seedfast&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup time&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Already there if you use Prisma&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install -g seedfast&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External dependency&lt;/td&gt;
&lt;td&gt;None — &lt;code&gt;psql&lt;/code&gt; is everywhere&lt;/td&gt;
&lt;td&gt;None — already in your stack&lt;/td&gt;
&lt;td&gt;Separate CLI plus a network call to Seedfast's generation service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File you maintain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;seed.sql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;seed.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None — reads the live schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FK order&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Survives migrations&lt;/td&gt;
&lt;td&gt;No — manual updates on every change&lt;/td&gt;
&lt;td&gt;No — manual updates on every change&lt;/td&gt;
&lt;td&gt;Yes — regenerates from the live schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realistic volumes&lt;/td&gt;
&lt;td&gt;Painful past ~50 rows&lt;/td&gt;
&lt;td&gt;Works with Faker, still hand-written&lt;/td&gt;
&lt;td&gt;Natural-language scope describes the dataset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output stability&lt;/td&gt;
&lt;td&gt;Deterministic — committed values&lt;/td&gt;
&lt;td&gt;Deterministic if you use fixed seeds&lt;/td&gt;
&lt;td&gt;Regenerated each run; pin a seed value when you need byte-stable fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewable in PRs&lt;/td&gt;
&lt;td&gt;Yes — diff the SQL&lt;/td&gt;
&lt;td&gt;Yes — diff the seed script&lt;/td&gt;
&lt;td&gt;Indirectly — the scope is reviewable, individual rows are not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good for static config (flags, codes)&lt;/td&gt;
&lt;td&gt;Excellent — versioned, reviewed&lt;/td&gt;
&lt;td&gt;Excellent — versioned, reviewed&lt;/td&gt;
&lt;td&gt;Not the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good for dev / CI / staging datasets&lt;/td&gt;
&lt;td&gt;Manual re-sync on every migration&lt;/td&gt;
&lt;td&gt;Manual re-sync on every migration&lt;/td&gt;
&lt;td&gt;Regenerates from the live schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What leaves your environment&lt;/td&gt;
&lt;td&gt;Nothing — runs locally&lt;/td&gt;
&lt;td&gt;Nothing — runs locally&lt;/td&gt;
&lt;td&gt;Schema metadata only (table and column shapes); no row data is transmitted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pick by job, not by religion. Reference data — the dozen rows the app reads at boot — belongs in a committed SQL file or a small &lt;code&gt;prisma db seed&lt;/code&gt; script that goes through code review with the schema change. Seedfast targets the larger, evolving dev / CI / staging datasets that go stale on every migration, where regenerating from the schema is faster than fixing the file. Free plan, no credit card — &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;try it on your Prisma Postgres schema&lt;/a&gt; in about two minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using @prisma/adapter-neon with local Postgres for development
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@prisma/adapter-neon&lt;/code&gt; is not required for local Postgres development. Use the default Prisma client with a regular &lt;code&gt;DATABASE_URL&lt;/code&gt;, or &lt;code&gt;@prisma/adapter-pg&lt;/code&gt; if you want a consistent driver-adapter API across environments.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.prisma.io/docs/orm/overview/databases/database-drivers" rel="noopener noreferrer"&gt;&lt;code&gt;@prisma/adapter-neon&lt;/code&gt;&lt;/a&gt; package exists to solve one specific problem: edge runtimes like Vercel Edge Functions and Cloudflare Workers can't open raw TCP sockets, so Prisma can't talk to Postgres the normal way. The adapter wraps Neon's serverless driver, which routes queries over HTTP and WebSockets to a Neon-hosted endpoint. None of that machinery applies when your code runs in plain Node against a local Postgres container — Node has TCP, the standard Prisma client connects directly, and there's no Neon endpoint to point the adapter at.&lt;/p&gt;

&lt;p&gt;For the common setup — Neon in production on the edge, plain Postgres in Docker locally — drop the adapter in development. Construct &lt;code&gt;new PrismaClient()&lt;/code&gt; with no &lt;code&gt;adapter&lt;/code&gt; argument, set &lt;code&gt;DATABASE_URL&lt;/code&gt; to a regular &lt;code&gt;postgresql://...&lt;/code&gt; string, and Prisma will talk to your local container over TCP. The Neon adapter only gets instantiated in production where it's actually needed.&lt;/p&gt;

&lt;p&gt;The trade-offs between the three options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Wire protocol&lt;/th&gt;
&lt;th&gt;Where it runs&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default &lt;code&gt;new PrismaClient()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;TCP (Prisma's built-in driver)&lt;/td&gt;
&lt;td&gt;Node only&lt;/td&gt;
&lt;td&gt;Local dev, seed scripts, any Node server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@prisma/adapter-pg&lt;/code&gt; (&lt;a href="https://www.prisma.io/docs/orm/overview/databases/database-drivers#postgresql" rel="noopener noreferrer"&gt;docs&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;TCP (wraps &lt;code&gt;node-postgres&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Node only&lt;/td&gt;
&lt;td&gt;You want the same driver-adapter API everywhere and locally use plain Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@prisma/adapter-neon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTP / WebSocket (wraps Neon's serverless driver)&lt;/td&gt;
&lt;td&gt;Edge (Vercel Edge, Cloudflare Workers)&lt;/td&gt;
&lt;td&gt;You deploy to an edge runtime against Neon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you'd rather keep one code path across environments — same constructor, same generic shape — use &lt;code&gt;@prisma/adapter-pg&lt;/code&gt; locally. It wraps &lt;code&gt;node-postgres&lt;/code&gt; (the standard &lt;code&gt;pg&lt;/code&gt; driver) and presents the same driver-adapter API as the Neon variant, so the only thing that changes between local and production is which adapter you import. Driver adapters reached GA in Prisma 6.16; on 6.16+, the &lt;code&gt;previewFeatures = ["driverAdapters"]&lt;/code&gt; flag in the &lt;code&gt;generator client&lt;/code&gt; block is no longer required. On older versions, add it to &lt;code&gt;schema.prisma&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conditional adapter: @prisma/adapter-pg locally, @prisma/adapter-neon in production
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Branch on &lt;code&gt;NODE_ENV&lt;/code&gt; inside the factory that constructs &lt;code&gt;PrismaClient&lt;/code&gt;: pass &lt;code&gt;PrismaPg&lt;/code&gt; locally and &lt;code&gt;PrismaNeon&lt;/code&gt; in production. The connection string and the query code stay identical; only the imported adapter changes.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/prisma.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaPg&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/adapter-pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaNeon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/adapter-neon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;makeClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaNeon&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaPg&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;schema.prisma&lt;/code&gt; needs the preview feature flag on Prisma versions where driver adapters aren't GA yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;generator&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"prisma-client-js"&lt;/span&gt;
  &lt;span class="n"&gt;previewFeatures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;"driverAdapters"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;datasource&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"postgresql"&lt;/span&gt;
  &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simpler alternative is to skip the adapter locally entirely. &lt;code&gt;new PrismaClient()&lt;/code&gt; with no adapter argument talks to local Postgres over plain TCP — nothing extra to install, configure, or fall out of sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  DATABASE_URL format for local Postgres with Prisma
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For local Postgres with Prisma, &lt;code&gt;DATABASE_URL&lt;/code&gt; is &lt;code&gt;postgresql://postgres:postgres@localhost:5432/postgres&lt;/code&gt; — plain TCP, no SSL, no &lt;code&gt;sslmode=require&lt;/code&gt;.&lt;/strong&gt; Replace user, password, and database name with whatever your container actually uses; both &lt;code&gt;postgresql://&lt;/code&gt; and &lt;code&gt;postgres://&lt;/code&gt; scheme prefixes work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env.local
&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;=&lt;span class="s2"&gt;"postgresql://postgres:postgres@localhost:5432/postgres"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things that catch people:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@prisma/adapter-neon&lt;/code&gt; will not work against this URL. The adapter expects a Neon HTTP/WebSocket endpoint (something like &lt;code&gt;postgresql://USER:PASS@ep-xxx.region.aws.neon.tech/dbname&lt;/code&gt;), and &lt;code&gt;localhost&lt;/code&gt; resolves to a plain Postgres server that doesn't speak Neon's protocol. If you point the Neon adapter at &lt;code&gt;localhost:5432&lt;/code&gt;, you'll get connection errors that look like protocol mismatches, not "wrong port" — because the bytes on the wire are different.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;schema.prisma&lt;/code&gt; declares &lt;code&gt;directUrl&lt;/code&gt;, set it to the same local URL during development. Prisma uses &lt;code&gt;directUrl&lt;/code&gt; for migrations and &lt;code&gt;prisma db seed&lt;/code&gt;, so leaving it pointed at a production Neon endpoint while &lt;code&gt;DATABASE_URL&lt;/code&gt; is local will run migrations against the wrong database the moment you forget which terminal you're in.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sslmode=require&lt;/code&gt; is for hosted Postgres (Neon, Prisma Postgres, Supabase, RDS). A local container doesn't have SSL configured by default, so leave &lt;code&gt;sslmode&lt;/code&gt; off entirely or set it to &lt;code&gt;disable&lt;/code&gt; if your client insists on a value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the seed step specifically, the local DATABASE_URL is what &lt;code&gt;prisma db seed&lt;/code&gt; reads when it constructs the client. No adapter, no &lt;code&gt;directUrl&lt;/code&gt; gymnastics — just point at the container and run. If the schema is past a dozen tables and the hand-written seed file is what's slowing the local loop down, &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; reads the live schema and generates the dataset directly against the local URL, so the development database fills up without a &lt;code&gt;seed.ts&lt;/code&gt; to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need @prisma/adapter-neon for local Postgres development?
&lt;/h3&gt;

&lt;p&gt;No. The Neon adapter exists for edge runtimes (Vercel Edge, Cloudflare Workers) that can't open TCP sockets. Local Node has TCP, so the default Prisma client connects to a local Postgres container directly. Use &lt;code&gt;new PrismaClient()&lt;/code&gt; with no &lt;code&gt;adapter&lt;/code&gt; argument and a regular &lt;code&gt;DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use @prisma/adapter-neon with localhost?
&lt;/h3&gt;

&lt;p&gt;Not in any useful way. The adapter expects Neon's HTTP/WebSocket endpoint (&lt;code&gt;*.neon.tech&lt;/code&gt;), and a local Postgres container doesn't speak that protocol — pointing the adapter at &lt;code&gt;localhost:5432&lt;/code&gt; produces protocol errors, not connection-refused errors. For local development, either drop the adapter and use the default client, or use &lt;code&gt;@prisma/adapter-pg&lt;/code&gt; which wraps &lt;code&gt;node-postgres&lt;/code&gt; and works against local TCP.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I switch between @prisma/adapter-pg locally and @prisma/adapter-neon in production?
&lt;/h3&gt;

&lt;p&gt;Branch on &lt;code&gt;NODE_ENV&lt;/code&gt; (or a dedicated env var) inside the factory that creates &lt;code&gt;PrismaClient&lt;/code&gt;. Import both adapters, pick the right constructor based on environment, and pass the resulting adapter to &lt;code&gt;new PrismaClient({ adapter })&lt;/code&gt;. The connection string can be the same env var (&lt;code&gt;DATABASE_URL&lt;/code&gt;); only the adapter import differs. Driver adapters are GA on Prisma 6.16+; on older versions, add &lt;code&gt;previewFeatures = ["driverAdapters"]&lt;/code&gt; to the &lt;code&gt;generator client&lt;/code&gt; block in &lt;code&gt;schema.prisma&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What DATABASE_URL should I use for local Postgres with Prisma?
&lt;/h3&gt;

&lt;p&gt;A plain TCP URL with no SSL: &lt;code&gt;postgresql://postgres:postgres@localhost:5432/postgres&lt;/code&gt; for the default Docker image, or whatever user, password, and database your container is configured with. Skip &lt;code&gt;sslmode=require&lt;/code&gt; — that's only for hosted Postgres (Neon, Prisma Postgres, Supabase). If &lt;code&gt;schema.prisma&lt;/code&gt; has a &lt;code&gt;directUrl&lt;/code&gt;, point it at the same local URL during development so migrations and seeds don't accidentally hit production.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I seed a Prisma Postgres database from the command line?
&lt;/h3&gt;

&lt;p&gt;Copy the &lt;strong&gt;direct&lt;/strong&gt; connection string from the Prisma Console (the one with &lt;code&gt;db.prisma.io&lt;/code&gt;, not &lt;code&gt;pooled.db.prisma.io&lt;/code&gt;) and run &lt;code&gt;psql "$DIRECT_URL" -f seed.sql&lt;/code&gt;. Make sure &lt;code&gt;sslmode=require&lt;/code&gt; is on the URL. For Prisma projects, configure &lt;code&gt;seed: "tsx prisma/seed.ts"&lt;/code&gt; in &lt;code&gt;prisma.config.ts&lt;/code&gt; and run &lt;code&gt;npx prisma db seed&lt;/code&gt;. For schemas with many tables, &lt;code&gt;seedfast seed --scope "..."&lt;/code&gt; generates connected data without writing a seed file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use the pooled or direct Prisma Postgres URL for seeding?
&lt;/h3&gt;

&lt;p&gt;Use the &lt;strong&gt;direct&lt;/strong&gt; URL (&lt;code&gt;db.prisma.io:5432&lt;/code&gt;). The pooled URL routes through a transaction-mode connection pooler that breaks prepared statements and can interrupt long seed transactions. Reserve the pooled URL for the application at runtime, where short, bursty queries benefit from the pool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I seed Prisma Postgres from a Cloudflare Worker or Vercel Edge?
&lt;/h3&gt;

&lt;p&gt;Yes — install &lt;code&gt;@prisma/adapter-ppg&lt;/code&gt;, pass a &lt;code&gt;PrismaPostgresAdapter&lt;/code&gt; instance to the &lt;code&gt;PrismaClient&lt;/code&gt; constructor, and the seed runs over Prisma's HTTP/WebSocket transport. The adapter takes the same direct TCP connection string from the Prisma Console; you don't need a separate URL. For seeds running on Node (CI, locally), the plain TCP path with &lt;code&gt;pg&lt;/code&gt; is simpler and one fewer dependency.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I seed Prisma Postgres in GitHub Actions?
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;DIRECT_URL&lt;/code&gt; as a repository secret pointing at the direct connection string. In the workflow, run &lt;code&gt;npx prisma migrate deploy&lt;/code&gt; against &lt;code&gt;DIRECT_URL&lt;/code&gt;, then run your seed step (either &lt;code&gt;npx prisma db seed&lt;/code&gt; or &lt;code&gt;seedfast seed --scope "..."&lt;/code&gt;). For application steps in the same job, use the pooled URL via &lt;code&gt;DATABASE_URL&lt;/code&gt;. The general pattern is covered in &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD database seeding&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I seed Prisma Postgres without writing a seed file?
&lt;/h3&gt;

&lt;p&gt;Connect Seedfast to the database (&lt;code&gt;seedfast connect&lt;/code&gt;, paste the direct URL) and run &lt;code&gt;seedfast seed --scope "&amp;lt;plain English description&amp;gt;"&lt;/code&gt;. Seedfast reads the live schema and generates a valid, connected dataset every time, so when migrations add columns or tables, the next seed run reflects the new shape automatically — there's no file that goes stale. See &lt;a href="https://seedfa.st/blog/database-seeder" rel="noopener noreferrer"&gt;database seeder tools compared&lt;/a&gt; for how this fits next to the framework-built-in seeders.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Prisma Postgres for staging without copying production data?
&lt;/h3&gt;

&lt;p&gt;Yes — that's the typical setup for teams in regulated industries. Stand up a separate Prisma Postgres database for staging, run migrations against it, then seed it with realistic generated data. No production rows leave the production environment, and the dev/staging path doesn't depend on an anonymization pipeline. Schema metadata is the only thing that crosses the wire to Seedfast's generation service, so review the data path against your security policy the same way you would any new vendor. &lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;Staging without production data&lt;/a&gt; walks through the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The short version: use &lt;code&gt;db.prisma.io:5432&lt;/code&gt; for seeds, never &lt;code&gt;pooled.db.prisma.io:5432&lt;/code&gt;. Configure &lt;code&gt;DIRECT_URL&lt;/code&gt; in &lt;code&gt;prisma.config.ts&lt;/code&gt; and reach for &lt;code&gt;@prisma/adapter-ppg&lt;/code&gt; only when the seed has to run on an edge runtime. Either pick a maintenance schedule for &lt;code&gt;seed.ts&lt;/code&gt; or hand the job to a tool that reads the schema on every run — what doesn't work is pretending a hand-written seed will keep up with twenty migrations a quarter.&lt;/p&gt;

&lt;p&gt;If a Friday afternoon spike on the seed file is what brought you here, the direct URL fixes today's break. The deeper fix is to stop hand-rolling the dataset every time the schema moves. Seedfast does that for Prisma Postgres in two minutes — &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;connect to your database&lt;/a&gt; and run a seed before you close the laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/seed-database" rel="noopener noreferrer"&gt;How to seed a database: PostgreSQL practical guide&lt;/a&gt; — the framework-agnostic version of this article, covering raw SQL, Prisma, Drizzle, TypeORM, and &lt;code&gt;node-postgres&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/database-seeding" rel="noopener noreferrer"&gt;Database seeding: methods and best practices&lt;/a&gt; — the conceptual companion covering reference vs test data, idempotency, and when seed files stop scaling&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/database-seeder" rel="noopener noreferrer"&gt;Database seeder tools compared&lt;/a&gt; — quick reference for Prisma, Drizzle, TypeORM, and standalone tools side by side&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/seed-file-maintenance" rel="noopener noreferrer"&gt;Seed file maintenance&lt;/a&gt; — why static seed files fall out of sync with the schema, and what to do about it&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/seed-neon-database" rel="noopener noreferrer"&gt;How to seed a Neon database&lt;/a&gt; — the Neon sibling to this guide, with the same pooler-vs-direct dance and a deep dive into branching&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/supabase-db-seed" rel="noopener noreferrer"&gt;How to seed a Supabase database&lt;/a&gt; — the Supabase sibling, covering &lt;code&gt;seed.sql&lt;/code&gt;, &lt;code&gt;supabase db reset&lt;/code&gt;, and Supabase preview branches&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;Staging database without production data&lt;/a&gt; — the compliance-driven workflow for fintech, healthcare, and anyone who can't pipeline PII into staging&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Get started with Seedfast&lt;/a&gt; — connect to your Prisma Postgres database and run your first schema-aware seed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/prisma-postgres-seed" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>prisma</category>
      <category>postgres</category>
      <category>node</category>
      <category>database</category>
    </item>
    <item>
      <title>Synthetic Data vs Real Data? First Say What It's For</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Fri, 14 Aug 2026 21:24:49 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/synthetic-data-vs-real-data-first-say-what-its-for-2md4</link>
      <guid>https://dev.to/mikh-shytsko/synthetic-data-vs-real-data-first-say-what-its-for-2md4</guid>
      <description>&lt;p&gt;You open the tool, and it wants a decision before you have said what the data is for. Generate the rows, or pull from what is real? Synthetic data vs real data gets framed as a head-to-head with one durable winner, so people go hunting for whichever source is generally safer or more faithful. No such winner exists. The comparison resolves only once you name the job the data is being hired for, because the property that makes a dataset excellent for one job barely registers for another.&lt;/p&gt;

&lt;p&gt;Most of the muddle comes from treating real as the default and synthetic as the fallback. Production data carries real authority, since it actually happened, but it earns its keep on one class of work and means little on another. Sort the work into two lanes, and the stalemate breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Synthetic data vs real data starts with the job
&lt;/h2&gt;

&lt;p&gt;Almost everything anyone generates or copies a database for lands in one of two lanes, and the lanes want opposite things.&lt;/p&gt;

&lt;p&gt;The first is statistical work, the training and evaluation of machine-learning models and the analytics beside them. Here the data stands in for a population, so what matters is that its overall shape matches reality closely enough. Correlations between columns hold, the numbers carry real skew, and a one-in-a-thousand case turns up about once in a thousand rows, and a few wrong rows in a set of millions do no damage, because the model reads the mass and not the exception.&lt;/p&gt;

&lt;p&gt;The second lane is the application database, the one your software connects to while you build against it, test it, stage it, or demo it — a lane that answers to a schema rather than to a distribution. Every foreign key must land on a row that exists; miss a constraint or leave a required column empty, and the database refuses the write and the work halts. So, a real-world distribution becomes a bonus here; validity against the schema is what the lane demands.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fseedfa.st%2Fblog%2Fsynthetic-data-vs-real-data-2.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fseedfa.st%2Fblog%2Fsynthetic-data-vs-real-data-2.svg" alt="Decision router for synthetic vs real data — statistical work is judged on distributional fidelity, application databases on relational correctness, and the winning source differs by lane" width="960" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The dividing line is error tolerance
&lt;/h2&gt;

&lt;p&gt;Underneath the two lanes sits the reason they diverge - a training corpus and an application database forgive mistakes at wildly different rates. Give a model a batch in which a small percentage of rows are wrong, and it still learns the pattern that dominates; the bad rows blur into noise the training averages over, and if the data improves later, the model improves with it. However, an application database grants no such margin, and a single row whose foreign key points at a customer that was never inserted is not an error to be averaged away - the insert is refused, the load stops there, and every test that expected that row fails for a reason unrelated to the code under test. Moreover, a statistical workload can name a defect rate it tolerates and run below it and a relational one has no rate to name.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the statistical lane, real data usually wins
&lt;/h2&gt;

&lt;p&gt;For work that feeds a model or a dashboard, real data holds a genuine edge, because you are chasing fidelity and real data is fidelity by definition. It carries the correlations nobody thought to encode and the messy tail nobody would have invented. When you can get it, and are allowed to use it, real data is often the right call for lane one.&lt;/p&gt;

&lt;p&gt;The catch lives in those two conditions: production data comes wrapped in access controls for good reason, and pointing a training pipeline at raw customer records is the exposure those controls exist to prevent, and it ages, since last quarter's snapshot describes a business that has moved on. Yes, it hands you only the cases that occurred, so a model needing thousands of examples of a rare fraud pattern gets the handful real data has and no more. Synthetic data is built for exactly that opening, producing as many rare cases as a model needs and carrying no real record. So, its weakness here is fidelity itself, the hard problem the fidelity-first synthesis platforms and open-source synthesizers spend their engineering on. The honest read on synthetic data vs production data in lane one is that synthetic wins when access is blocked or rare cases are scarce, and loses when its fidelity falls short of the real distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The application lane flips the answer
&lt;/h2&gt;

&lt;p&gt;Populate a database your application runs against and the calculus inverts. Fidelity stops being the point; correctness against the schema becomes the whole of it. You need a dataset where every reference resolves and every constraint holds, so the app boots and the tests run instead of dying on a bad insert. Real production data can clear that bar, but dragging a copy into a development or staging environment looks safe and is not. It brings the exposure problem back where the controls are weakest, a full clone is heavy enough to slow every reset, and it drifts from the schema the moment someone ships a migration the dump predates. It also cannot manufacture the case you need on demand, since production has the customers it has, not the five hundred with a canceled subscription and refunded order your edge-case test wants.&lt;/p&gt;

&lt;p&gt;Synthetic data is the natural fit here, with one caveat that trips teams up. A generator filling each column from its own source of randomness hands you a users table and an orders table that look perfect alone and do not connect, order rows pointing at customer IDs that were never created, which a real foreign key rejects on the first insert. Why believable values are the easy half while making the rows connect is the hard half is the subject of &lt;a href="https://seedfa.st/blog/synthetic-test-data-generation" rel="noopener noreferrer"&gt;how synthetic generation actually works end to end&lt;/a&gt;; the deeper version, where a reference resolves and still means nonsense, runs through &lt;a href="https://seedfa.st/blog/realistic-test-data" rel="noopener noreferrer"&gt;what coherent data really demands&lt;/a&gt;. Some teams split the difference by masking a production copy, which trades one set of headaches for another and sits beside the other options in the &lt;a href="https://seedfa.st/blog/data-seeding-tools" rel="noopener noreferrer"&gt;seeding-tool landscape&lt;/a&gt;. Real data, for all its authenticity, is out of place in this lane, and naive synthetic data fits the lane while still breaking the schema — so the version worth having reads the schema first and generates rows that already connect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison, by job
&lt;/h2&gt;

&lt;p&gt;Laid side by side, the two lanes ask nearly opposite things of the same two sources.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Statistical lane (models, analytics)&lt;/th&gt;
&lt;th&gt;Application lane (dev, test, staging, demos)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What the data stands in for&lt;/td&gt;
&lt;td&gt;a population's distribution&lt;/td&gt;
&lt;td&gt;a schema the software runs against&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The bar for "good"&lt;/td&gt;
&lt;td&gt;distributional fidelity&lt;/td&gt;
&lt;td&gt;relational correctness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost of one wrong row&lt;/td&gt;
&lt;td&gt;absorbed into a defect rate&lt;/td&gt;
&lt;td&gt;halts the insert and every test after it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real data's main liability&lt;/td&gt;
&lt;td&gt;access limits, staleness, no rare cases on demand&lt;/td&gt;
&lt;td&gt;exposure risk, size, drift, no edge cases on demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Synthetic data's main liability&lt;/td&gt;
&lt;td&gt;fidelity gaps against the real distribution&lt;/td&gt;
&lt;td&gt;naive generators that ignore the schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usually the better source&lt;/td&gt;
&lt;td&gt;real, or high-fidelity synthetic when access is blocked&lt;/td&gt;
&lt;td&gt;synthetic that reads the schema&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read it down a column for your job, not across a row for a winner. If the data feeds a model or a report, start from real and turn to synthetic when access rules or missing rare cases force it, then judge what you generate on how faithfully it reproduces the distribution. If it populates an application's database, start from synthetic and hold it to one standard above the rest, that every row satisfies the schema. The synthetic vs real test data decision comes down to exactly that. Doing it well is its own discipline, the one &lt;a href="https://seedfa.st/blog/database-seeding" rel="noopener noreferrer"&gt;database seeding&lt;/a&gt; covers, and the AI-driven options are ranked in &lt;a href="https://seedfa.st/blog/best-ai-test-data-generator" rel="noopener noreferrer"&gt;the AI generator comparison&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Seedfast fits, and where it doesn't
&lt;/h2&gt;

&lt;p&gt;Seedfast works one lane. It is data infrastructure for the application database, the development, testing, staging, and demo environments where relational correctness is the bar and a copy of production is a liability. It connects to a live PostgreSQL database, reads the current schema, and generates connected, referentially consistent rows that fit it, so a child row lands on a parent that exists rather than on an ID that was never created. Describe what you want in plain language and it fills the database in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"500 customers with orders, payments, and a realistic spread of account ages"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because those rows are invented from the schema and never sampled from production, no production access is required and no production rows are involved, which is why a generated dev or staging database sidesteps the copy-of-production liability above. That is a consequence of generating rather than copying, not a compliance claim. Seedfast is not built for lane one; it does not train your models or feed an analytics warehouse a replica of production statistics, work that wants a different tool and a different definition of good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is synthetic data as good as real data?
&lt;/h3&gt;

&lt;p&gt;It depends entirely on the job. On statistical work, where fidelity to a real distribution is what counts, carefully sampled real data is the benchmark and synthetic is judged by how near it gets. On an application database, where all that matters is that every row satisfies the schema, a copy of production is more hazard than help and well-formed synthetic data wins outright.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does synthetic data work for training machine-learning models?
&lt;/h3&gt;

&lt;p&gt;Yes, and that is squarely lane one. A model learns the pattern that dominates its training set, so it tolerates a defect rate that would wreck an application database, which is why generated data can train a working model even when it is imperfect. Two things decide how well it works. One is how faithfully the synthetic set reproduces the real distribution, rare cases included. The other is whether you validated the result on held-out real data rather than on more synthetic data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I just use a copy of production for my test database?
&lt;/h3&gt;

&lt;p&gt;You can, and plenty of teams do, but it hides its costs. A production copy carries real customer data into environments with weaker controls, weighs enough to slow every reset, and goes stale the day someone ships a migration. Worse for testing, it holds only the cases that already happened, so the edge case your feature needs may not be there at all. The synthetic data vs production data trade-off usually favors generating the rows you need over importing the ones you have.&lt;/p&gt;

&lt;h3&gt;
  
  
  When is real data the right choice?
&lt;/h3&gt;

&lt;p&gt;Whenever the job is statistical and you are cleared to use it. Model training, evaluation, and analytics all read the aggregate and absorb a few odd rows, so they benefit from the fidelity only real data carries for free. The two gates are permission and freshness; fail either and high-fidelity synthetic data steps in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Match the source to the job
&lt;/h2&gt;

&lt;p&gt;The reason "synthetic data vs real data" stays unsettled in the abstract is that it was never one question. It is at least two, wearing the same words. Sort your work into the statistical lane or the application lane, check which property decides quality there, and the choice that felt like a coin toss turns fairly obvious. For the application lane, where a single dangling reference takes down the whole run, Seedfast reads your live schema and generates connected data without touching production. &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;Run your first seed&lt;/a&gt; in a couple of minutes, or read the &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; first, a free plan that asks for no card and then flat plans at $16 and $69 a month.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/synthetic-data-vs-real-data" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>testing</category>
      <category>postgres</category>
      <category>ai</category>
    </item>
    <item>
      <title>Pytest Database Fixtures That Stay Fast and Honest</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sat, 18 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/pytest-database-fixtures-that-stay-fast-and-honest-1glf</link>
      <guid>https://dev.to/mikh-shytsko/pytest-database-fixtures-that-stay-fast-and-honest-1glf</guid>
      <description>&lt;p&gt;A test that talks to Postgres has to get its data from somewhere, and most pytest suites make that call badly. Either every test rebuilds the whole database and the run crawls, or a pile of hand-written setup fixtures drifts away from the schema until one Tuesday migration turns a wall of green tests red. Pytest database fixtures are supposed to be the fix here, not the source of the pain. They're dependency injection for your tests — a way to declare "this test needs a database in this state" and let pytest wire it up. Things go wrong when a fixture quietly becomes the place you park slow I/O or a stack of hardcoded rows.&lt;/p&gt;

&lt;p&gt;This guide walks the pieces that decide whether a database suite stays fast and trustworthy — what the fixture scopes actually cost, the transactional rollback pattern that spares a Postgres suite from truncating tables between tests, and where seeding a realistic baseline belongs so your fixtures stop hauling around data they were never meant to own. The code assumes SQLAlchemy 2.0 and pytest-django 4.x, the current stable lines of both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A fixture is dependency injection, not a hook for slow setup.&lt;/strong&gt; The moment a fixture starts rebuilding a database on every call, the scope you gave it is the whole cost of your suite.&lt;/li&gt;
&lt;li&gt;Scope is the lever nobody adjusts. A session-scoped connection built once and reused is the difference between a suite that finishes in seconds and one that reconnects to Postgres a thousand times.&lt;/li&gt;
&lt;li&gt;Roll back, don't truncate. Wrapping each test in a transaction and rolling it back at teardown gives you a clean slate without touching a single &lt;code&gt;TRUNCATE&lt;/code&gt;, and SQLAlchemy 2.0 makes the recipe short.&lt;/li&gt;
&lt;li&gt;Deferred foreign keys are the trap in that pattern — a constraint marked &lt;code&gt;DEFERRABLE INITIALLY DEFERRED&lt;/code&gt; only fires at commit, and a rollback fixture never commits, so it can hide a broken row your production code would reject.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; seeds a realistic, schema-consistent baseline once&lt;/strong&gt; — locally or as a step before the test job in CI — so pytest's own scoping and rollback handle isolation on top of it. There's no plugin to install into the fixture path and no per-test call out to a generator.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why pytest database fixtures go wrong before they go slow
&lt;/h2&gt;

&lt;p&gt;Speed is the symptom people notice, but correctness usually breaks first. A fixture that inserts &lt;code&gt;user_id=42&lt;/code&gt; and asserts against it works fine until another test deletes user 42, or CI starts its sequences from a different number, or two parallel tests both claim the same primary key. The failure message then reads &lt;code&gt;expected 3, got 0&lt;/code&gt; and points at application code that has nothing to do with the real problem, which is that the data the test assumed is gone.&lt;/p&gt;

&lt;p&gt;Shared mutable state is the root of nearly all of it. When several tests read and write the same rows, order starts to matter, and a suite where order matters can't run in parallel. A &lt;code&gt;tearDown&lt;/code&gt; that deletes what the test created looks tidy until one test raises halfway through, skips its cleanup, and leaves rows behind that quietly poison everything after it. What each test actually needs is a starting state it can trust and a guarantee that nothing it writes leaks into the next one — scope and transactional rollback are how you get both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixture scopes: function, class, module, session
&lt;/h2&gt;

&lt;p&gt;Every pytest fixture has a scope, and scope decides how often the setup and teardown code runs. The pytest docs list five values — &lt;code&gt;function&lt;/code&gt;, &lt;code&gt;class&lt;/code&gt;, &lt;code&gt;module&lt;/code&gt;, &lt;code&gt;package&lt;/code&gt;, and &lt;code&gt;session&lt;/code&gt; — ordered from narrowest to broadest. A &lt;code&gt;function&lt;/code&gt; fixture (the default) is torn down at the end of each test. A &lt;code&gt;session&lt;/code&gt; fixture is built once when first requested and destroyed only when the whole run ends.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# the default — rebuilt for every test
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ada@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;module&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# once per test file
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;api_client&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# once for the entire run
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;
    &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For database work the choice is not cosmetic. Building a SQLAlchemy &lt;code&gt;Engine&lt;/code&gt; and opening a Postgres connection is expensive enough to dominate the clock on any suite past a few dozen tests, so the engine and any read-only reference data belong at session scope, while the per-test transaction that isolates writes belongs at function scope. The table below is the trade each level makes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Setup runs&lt;/th&gt;
&lt;th&gt;Reuse is safe when&lt;/th&gt;
&lt;th&gt;Reuse bites when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;function&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every test&lt;/td&gt;
&lt;td&gt;Always — full isolation, no shared state&lt;/td&gt;
&lt;td&gt;Never a correctness risk, only a speed one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;class&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Once per test class&lt;/td&gt;
&lt;td&gt;Tests in the class only read the shared value&lt;/td&gt;
&lt;td&gt;A test mutates it and a later one depends on the original&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;module&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Once per file&lt;/td&gt;
&lt;td&gt;The fixture is a connection, engine, or read-only data&lt;/td&gt;
&lt;td&gt;Tests in the file write to it expecting a clean slate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Once per run&lt;/td&gt;
&lt;td&gt;The value is genuinely immutable or self-cleaning&lt;/td&gt;
&lt;td&gt;Any test mutates it — the mutation leaks to every later test&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern that holds up keeps the expensive, stable things at a broad scope and layers a narrow-scoped fixture on top for isolation — the trick behind the rollback pattern below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transactional rollback pattern
&lt;/h2&gt;

&lt;p&gt;Here is the single most useful pattern for a Postgres test suite, and the one thin tutorials skip. Instead of resetting the database between tests, you open a transaction before each test, run the test inside it, and roll it back afterward. Postgres then discards everything the test wrote and hands the next test the same baseline it started from, without truncating a single table. A rollback is far cheaper than a &lt;code&gt;TRUNCATE ... CASCADE&lt;/code&gt; plus a reseed, so the suite stays fast as it grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQLAlchemy: bind a Session to an outer transaction
&lt;/h3&gt;

&lt;p&gt;The catch is that ORM code wants to call &lt;code&gt;session.commit()&lt;/code&gt;, and a naive setup would commit straight through to the database and defeat the rollback, a problem SQLAlchemy heads off with SAVEPOINTs. You begin a real transaction on a raw connection, bind the &lt;code&gt;Session&lt;/code&gt; to that connection, and tell the session to implement its own &lt;code&gt;commit&lt;/code&gt;/&lt;code&gt;rollback&lt;/code&gt; as SAVEPOINTs inside your outer transaction.&lt;/p&gt;

&lt;p&gt;In SQLAlchemy 2.0 this is one parameter. The official recipe for &lt;a href="https://docs.sqlalchemy.org/en/20/orm/session_transaction.html" rel="noopener noreferrer"&gt;joining a Session into an external transaction&lt;/a&gt; passes &lt;code&gt;join_transaction_mode="create_savepoint"&lt;/code&gt;, and the docs note that the event handlers older versions needed to reset the nested transaction are no longer required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_engine&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;

&lt;span class="n"&gt;TEST_DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql+psycopg2://app:app@localhost:5432/app_test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;
    &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# outer transaction, never committed
&lt;/span&gt;    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;join_transaction_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create_savepoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# discard everything the test wrote
&lt;/span&gt;        &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test that uses &lt;code&gt;db_session&lt;/code&gt; can insert rows, call &lt;code&gt;session.commit()&lt;/code&gt; as many times as its code path needs, and read back exactly what committed — all against a SAVEPOINT. When the fixture tears down, the outer &lt;code&gt;transaction.rollback()&lt;/code&gt; throws the whole thing away. Because the engine is session-scoped and only &lt;code&gt;db_session&lt;/code&gt; is function-scoped, you pay for the connection a single time while every test still gets its own clean transaction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fseedfa.st%2Fblog%2Fpytest-database-fixtures-2.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fseedfa.st%2Fblog%2Fpytest-database-fixtures-2.svg" alt="The SQLAlchemy rollback pattern — session.commit() creates savepoints inside an outer transaction the fixture rolls back at teardown, discarding every write" width="960" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  pytest-django: the django_db marker
&lt;/h3&gt;

&lt;p&gt;If you're on Django, pytest-django gives you the same isolation without wiring the transaction yourself. Mark a test with &lt;code&gt;@pytest.mark.django_db&lt;/code&gt; and, per the &lt;a href="https://pytest-django.readthedocs.io/en/stable/database.html" rel="noopener noreferrer"&gt;pytest-django database docs&lt;/a&gt;, it runs inside a transaction that is rolled back at the end — the same mechanism Django's own &lt;code&gt;TestCase&lt;/code&gt; uses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.contrib.auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_user_model&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.mark.django_db&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_new_user_is_active&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_user_model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ada@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This marker has one important exception worth flagging. When your code relies on &lt;code&gt;transaction.on_commit()&lt;/code&gt; hooks, or a test spins up the &lt;code&gt;live_server&lt;/code&gt; fixture, or you specifically want to assert real commit-and-rollback behavior, the rollback wrapper works against you, because nothing ever commits and those paths never fire. For those cases pass &lt;code&gt;transaction=True&lt;/code&gt;, which flushes the database between tests instead of wrapping them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.django_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_email_is_queued_on_commit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# on_commit hooks actually run here, because the test really commits
&lt;/span&gt;    &lt;span class="nf"&gt;register_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ada@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs are blunt about the cost. Flushing the database between tests runs much slower than rolling a transaction back, so reach for &lt;code&gt;transaction=True&lt;/code&gt; only when you're actually testing transaction behavior, and default to the plain marker everywhere else.&lt;/p&gt;

&lt;h3&gt;
  
  
  The deferred-constraint edge case
&lt;/h3&gt;

&lt;p&gt;Rollback isolation has one failure mode worth knowing before it bites you in production. A foreign key declared &lt;code&gt;DEFERRABLE INITIALLY DEFERRED&lt;/code&gt; is not checked when you run the &lt;code&gt;INSERT&lt;/code&gt;; PostgreSQL's docs state plainly that "DEFERRED constraints are not checked until transaction commit." A rollback fixture never commits its outer transaction, and even the &lt;code&gt;session.commit()&lt;/code&gt; calls inside a test only release SAVEPOINTs rather than committing the top-level transaction. So a row that dangles a deferred foreign key at nothing will sail through a rollback-isolated test and then blow up the first time real application code commits it.&lt;/p&gt;

&lt;p&gt;When a test genuinely needs to exercise that deferred check, force it with &lt;code&gt;SET CONSTRAINTS ALL IMMEDIATE&lt;/code&gt; at the point you want the constraint evaluated, or run that specific test with real commits via &lt;code&gt;@pytest.mark.django_db(transaction=True)&lt;/code&gt; instead of the rollback path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- inside the test's transaction, before you expect the check to fire&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINTS&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;IMMEDIATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- a dangling deferred FK now raises here instead of hiding until commit&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most suites never hit this, because most schemas don't defer their constraints. If yours does — cyclic references between two tables are the usual reason — keep at least one commit-based test on that path so the rollback pattern isn't silently covering for a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seed the baseline once, not per test
&lt;/h2&gt;

&lt;p&gt;Rollback keeps tests from stepping on each other, but it puts nothing in the database to begin with. Something still has to establish the realistic starting state your integration tests query against — the customers and their orders, the reference tables they join to — and the wrong place to do that is inside a per-test fixture that regenerates it every time.&lt;/p&gt;

&lt;p&gt;Seed it once, ahead of the run — one CLI command before you invoke pytest locally, or a step after migrations and before the test job in CI. &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; reads your live schema and generates realistic, connected rows that satisfy its constraints, so a single &lt;code&gt;seedfast seed&lt;/code&gt; fills the whole database with a baseline your tests can read without you hand-writing a row of it. Pytest's scoping and the rollback fixture take it from there, every test seeing the baseline and rolling back its own writes on top.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conftest.py — assert the baseline is present, fail loud if it isn't
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;seeded_baseline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT count(*) FROM customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;scalar_one&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Test database is empty. Seed a baseline first:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; seedfast seed --scope &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;a few customers with orders and line items&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;returncode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;autouse&lt;/code&gt; session fixture doesn't generate anything — it checks that the baseline seeded outside the test run actually landed and stops the suite with a useful message if someone forgot to seed. Compare that shape with the alternative some tools push, where every fixture calls out to a generation service at test time and couples your setup to a network call on the hot path. One seed step up front, described in plain English with the &lt;a href="https://seedfa.st/docs/scoping" rel="noopener noreferrer"&gt;&lt;code&gt;--scope&lt;/code&gt; flag&lt;/a&gt;, keeps generation out of the loop, and the &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;getting-started guide&lt;/a&gt; covers &lt;code&gt;seedfast connect&lt;/code&gt; and &lt;code&gt;seedfast seed&lt;/code&gt; for the first run.&lt;/p&gt;

&lt;h2&gt;
  
  
  factory_boy vs. raw SQL fixtures vs. schema-aware seeding
&lt;/h2&gt;

&lt;p&gt;These three approaches solve overlapping problems, and the honest answer is that a mature suite often uses more than one. The question is which job each is actually good at.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Best at&lt;/th&gt;
&lt;th&gt;Cost on a schema change&lt;/th&gt;
&lt;th&gt;Foreign keys &amp;amp; constraints&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw SQL fixtures&lt;/td&gt;
&lt;td&gt;Tiny, fixed reference data you rarely touch&lt;/td&gt;
&lt;td&gt;Manual edit every migration; drifts silently&lt;/td&gt;
&lt;td&gt;You order the inserts and satisfy every FK by hand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;factory_boy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The one specific object a test asserts on&lt;/td&gt;
&lt;td&gt;Edit the factory when its model changes&lt;/td&gt;
&lt;td&gt;Coded per relationship via &lt;code&gt;SubFactory&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema-aware baseline (Seedfast)&lt;/td&gt;
&lt;td&gt;The realistic bulk every test queries against&lt;/td&gt;
&lt;td&gt;None — it re-reads the schema each run&lt;/td&gt;
&lt;td&gt;Handled from the live schema; values aren't repeatable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Raw SQL fixtures are the fastest thing to write and the first to rot. A hand-maintained &lt;code&gt;INSERT&lt;/code&gt; script freezes the table shape on the day you wrote it, and the next &lt;code&gt;NOT NULL&lt;/code&gt; column turns it into a failing insert three layers removed from the test. &lt;a href="https://factoryboy.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;&lt;code&gt;factory_boy&lt;/code&gt;&lt;/a&gt; earns its place for the object under test — when a test needs "an admin user with a suspended subscription", a factory builds precisely that, with sequences and &lt;code&gt;SubFactory&lt;/code&gt; relationships, and reads clearly at the call site.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;myapp.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;django&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DjangoModelFactory&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;

    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;member&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="c1"&gt;# in a test: the exact object this test asserts on
&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where factory_boy stops paying is the background. Populating fifty tables of realistic, foreign-key-valid data so a reporting query has something to run against means hand-defining a factory per table and keeping each in step with migrations — the volume a schema-aware baseline covers without the upkeep. The two are complementary rather than rival. Seed the connected bulk once, build the single asserted-on object with a factory inside the test, and treat it as &lt;a href="https://seedfa.st/blog/test-data-management" rel="noopener noreferrer"&gt;test data management&lt;/a&gt; split by job rather than one tool doing both.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to seed per-test vs. once-per-suite
&lt;/h2&gt;

&lt;p&gt;Default to seeding once and isolating with rollback. It's the fast path, and it fits the large majority of integration tests, which read the baseline, write a little, assert, and don't care whether their neighbor's rows exist. Session-scoped baseline plus function-scoped transactional rollback is the shape to reach for first.&lt;/p&gt;

&lt;p&gt;Re-seeding for an individual test earns its cost only when the test needs a genuinely different data shape than the baseline provides. An empty-state UI test wants a customer with zero orders. For pagination you might need one customer sitting on two hundred, and a permissions test often depends on a specific matrix of roles the baseline doesn't happen to contain. In each of those, build the exact rows with a factory inside a function-scoped fixture, still under the rollback so the special-case data disappears at teardown.&lt;/p&gt;

&lt;p&gt;The trap is the middle ground where a suite re-seeds the whole database per test out of caution — that's how a five-minute run becomes forty. If a test queries for "a customer with at least one order" instead of assuming customer 42, it can share the baseline, and most tests can be written that way with a little discipline about not hardcoding IDs. The &lt;a href="https://seedfa.st/blog/e2e-test-fixtures" rel="noopener noreferrer"&gt;E2E test fixtures&lt;/a&gt; guide works the same principle from the browser-testing side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it in CI
&lt;/h2&gt;

&lt;p&gt;The CI story is short because most of it isn't pytest-specific. You bring up a Postgres service container, gate it on &lt;code&gt;pg_isready&lt;/code&gt; so no step races a database that isn't listening, migrate, seed the baseline, then run pytest. The seed step lands last, immediately before the tests read the database. The YAML below uses Seedfast as that seed step; any seed script slots into the same position.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alembic upgrade head&lt;/span&gt; &lt;span class="c1"&gt;# migrate first&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql+psycopg2://app:app@localhost:5432/app_test&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx seedfast seed --scope "a few customers with orders and line items" --output json&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SEEDFAST_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEEDFAST_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;SEEDFAST_DSN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://app:app@localhost:5432/app_test&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest -q&lt;/span&gt; &lt;span class="c1"&gt;# then read the database&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;TEST_DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql+psycopg2://app:app@localhost:5432/app_test&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fadsdainm1asa7yvcj13s.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fadsdainm1asa7yvcj13s.webp" alt="GitHub Actions job where alembic upgrade head, seedfast seed, and pytest -q run in order and finish green against a Postgres service container" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The full mechanics of the service container, the health-check gate, and the migrate-then-seed ordering get their own walkthrough in &lt;a href="https://seedfa.st/blog/github-actions-seed-postgres-database" rel="noopener noreferrer"&gt;seeding a Postgres test database in GitHub Actions&lt;/a&gt; — there's no reason to repeat it here. The Seedfast-specific CI setup, including the API key and non-interactive scoping, lives in the &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD database seeding docs&lt;/a&gt;. Whichever way you seed, the running order is the load-bearing part, and this is &lt;a href="https://seedfa.st/blog/database-seeding" rel="noopener noreferrer"&gt;database seeding&lt;/a&gt; applied to a pytest job like any other.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete conftest.py, end to end
&lt;/h2&gt;

&lt;p&gt;The complete file below wires together a session-scoped engine, an &lt;code&gt;autouse&lt;/code&gt; guard that fails fast when the baseline is missing, and the function-scoped pytest Postgres fixture (&lt;code&gt;db_session&lt;/code&gt;) that every test depends on. Drop it at the root of your test tree and the whole pytest test database setup fits in one file: seed once before the run, and each test gets the baseline plus perfect isolation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conftest.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;

&lt;span class="n"&gt;TEST_DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TEST_DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql+psycopg2://app:app@localhost:5432/app_test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Built once for the whole run — opening connections is the expensive part.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;
    &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;seeded_baseline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Assert the baseline seeded before pytest ran (locally via the CLI, or a
    prior CI step) is actually present. Fail loud rather than run empty tests.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT count(*) FROM customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;scalar_one&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Test database is empty. Seed a baseline first:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; seedfast seed --scope &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;a few customers with orders and line items&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;returncode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Function-scoped session bound to an outer transaction that is always
    rolled back. Tests may call session.commit() freely — it commits to a
    SAVEPOINT, and the outer rollback discards everything at teardown.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;join_transaction_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create_savepoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test then reads like plain application code, and the isolation is invisible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_order_total_sums_line_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id FROM customers LIMIT 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;scalar_one&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO orders (customer_id, status) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALUES (:cid, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) RETURNING id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# commits to a SAVEPOINT, not the real database
&lt;/span&gt;
    &lt;span class="n"&gt;open_orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT count(*) FROM orders WHERE status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;scalar_one&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;open_orders&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="c1"&gt;# teardown rolls the outer transaction back — the next test never sees this order
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it, and the moment the fixture tears down, the order this test created is gone. There's nothing to clean up or truncate, and the baseline the whole suite reads against sits exactly as &lt;code&gt;seedfast seed&lt;/code&gt; left it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need a pytest plugin to seed a database?
&lt;/h3&gt;

&lt;p&gt;No. Seeding and test isolation are separate jobs, and pytest already handles the isolation half natively through fixture scopes and transactional rollback. For the seeding half you only need something that fills the database before the run — a CLI command locally, a step in CI — not a plugin wired into your fixture path. Seedfast runs as that one seed step and leaves the fixtures to pytest, so there's nothing extra to install into the test process itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between a pytest fixture and database seeding?
&lt;/h3&gt;

&lt;p&gt;A fixture is a pytest construct that provides a test with what it needs and cleans up afterward — a connection, a session, an object under test. Seeding is the separate act of putting a realistic starting dataset into the database before any of that runs. A fixture can assume the seeded data exists and query it, but it shouldn't be regenerating that dataset on every test, which is what makes a suite slow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use transaction rollback or truncate tables between tests?
&lt;/h3&gt;

&lt;p&gt;Prefer rollback. A rollback discards the test's writes in one step, while a truncate-and-reseed pays table-by-table I/O between every pair of tests — and with SQLAlchemy's &lt;code&gt;join_transaction_mode="create_savepoint"&lt;/code&gt; your ORM code can still call &lt;code&gt;commit()&lt;/code&gt; freely. Truncation is only the better tool when you must test real commit behavior — &lt;code&gt;on_commit&lt;/code&gt; hooks, cross-connection visibility — which is exactly the case pytest-django covers with &lt;code&gt;@pytest.mark.django_db(transaction=True)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does factory_boy replace database seeding?
&lt;/h3&gt;

&lt;p&gt;Not for the bulk of your data. factory_boy is excellent for building the specific object a test asserts on, with sequences and &lt;code&gt;SubFactory&lt;/code&gt; relationships. It's a poor fit for populating fifty foreign-key-linked tables of realistic background data, because that means writing and maintaining a factory per table against every migration. Seed the connected volume once with a schema-aware step, and use factory_boy for the one-off object each test cares about — the two work together.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I seed a Postgres test database for pytest in CI?
&lt;/h3&gt;

&lt;p&gt;Run the seed step after migrations and before pytest. Bring up a Postgres service container, gate it with &lt;code&gt;pg_isready&lt;/code&gt;, apply your migrations, then run &lt;code&gt;npx seedfast seed&lt;/code&gt; against the CI database with &lt;code&gt;SEEDFAST_API_KEY&lt;/code&gt; and a connection string, and finally invoke pytest pointed at the same database. The &lt;a href="https://seedfa.st/blog/github-actions-seed-postgres-database" rel="noopener noreferrer"&gt;GitHub Actions guide&lt;/a&gt; covers the container and health-gate mechanics, and the &lt;a href="https://seedfa.st/docs/cicd-database-seeding" rel="noopener noreferrer"&gt;CI/CD database seeding docs&lt;/a&gt; cover the key and non-interactive scoping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to take it from here
&lt;/h2&gt;

&lt;p&gt;The shape that holds up is layered. At the bottom sit a session-scoped engine and a baseline seeded once; on top of them, function-scoped transactional rollback provides isolation while a factory handles the single object a given test asserts on. Get that stack right and your fixtures stop being a maintenance surface — they go back to being plain dependency injection, which is all they were ever meant to be.&lt;/p&gt;

&lt;p&gt;If the "seed a realistic baseline once" step is the piece you're missing, point Seedfast at a local or CI database and let it read the schema and fill it. The &lt;a href="https://seedfa.st/docs/seed-your-database" rel="noopener noreferrer"&gt;getting-started guide&lt;/a&gt; takes you from &lt;code&gt;seedfast connect&lt;/code&gt; to a seeded database, and the &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;free plan&lt;/a&gt; doesn't ask for a card.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/pytest-database-fixtures" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>pytest</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Demo Data Looks Real Until Someone Clicks Into It</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sat, 18 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/demo-data-looks-real-until-someone-clicks-into-it-gfl</link>
      <guid>https://dev.to/mikh-shytsko/demo-data-looks-real-until-someone-clicks-into-it-gfl</guid>
      <description>&lt;p&gt;The account looks lived-in — forty companies down the sidebar, avatars scattered across a team view, invoices going back six months, the kind of screen you'd happily drop into a pitch deck. Then the prospect clicks into one of those invoices on the call, and the customer it's billed to isn't anywhere in the customer list. Upstream, a demo data generator had filled every table with believable rows and never once checked whether a row in one table points at a row that exists in another.&lt;/p&gt;

&lt;p&gt;That specific failure is what this page is about. Filling a database so it reads as real to a human is a different problem from producing a flat file of plausible values, and most tools that surface when you search for a demo data generator solve the second problem cleanly while leaving the first untouched. They're genuinely good at the flat file, and they run out of road the moment your demo spans tables that reference each other, which — for anything shaped like a real product — happens on the first click.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Demo data and test data are different jobs. Demo data has to look coherent to a person watching a screen; test data has to trip the edge cases a CI check reads, and no one will ever look at it.&lt;/li&gt;
&lt;li&gt;The flat generators that rank for this term — Mockaroo, generatedata.com, Beeceptor — fill one table at a time, so foreign keys between tables don't line up. That's fine for a single CSV or a mock API, and a real problem once a demo spans related tables.&lt;/li&gt;
&lt;li&gt;A convincing demo has shape: a few large accounts over a long tail of small ones, signup dates that precede activity, invoice totals that reconcile with their line items.&lt;/li&gt;
&lt;li&gt;"Demo data for a SaaS" is not the same as a sales-demo-automation platform such as Reprise or Demostack. Those personalize the sales pitch itself and are typically a much larger purchase than a data generator.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; generates relational demo data straight against your application's live schema in a single run. The free plan takes no card.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What "demo data" actually means
&lt;/h2&gt;

&lt;p&gt;Demo data is data you load into your own product's database so the thing looks real to whoever is watching it. A prospect clicking through a trial account, an investor sitting through a walkthrough, a new user poking at a sandbox on day one — each of them is judging the product partly on whether its data feels like a real company has been using it. Test data answers a narrower question. It exists to drive assertions, so it cares about the null field, the boundary value, and the one row that makes a CI check go red, and nobody ever admires how it looks.&lt;/p&gt;

&lt;p&gt;The two overlap enough that people reach for the same tools for both, then get surprised when a generator tuned for one disappoints at the other. A dataset built to break a pagination bug rarely reads like a tidy sales demo, and one assembled to charm a prospect won't go hunting for the ugly corner your test suite lives for.&lt;/p&gt;

&lt;p&gt;One disambiguation is worth making before we go further, because this term pulls two very different searchers. If what you want is software that assembles and personalizes the &lt;em&gt;sales pitch itself&lt;/em&gt; — swapping in a prospect's logo, staging a guided product tour, tracking who watched what — that is a sales-demo-automation platform like Reprise, Demostack, or Saleo, and it's a much larger purchase aimed at revenue teams. This page is about the other job of getting realistic rows into your own application's database. If you landed here looking for the pitch-automation category, that's a different shelf entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why flat row generators break the moment tables relate
&lt;/h2&gt;

&lt;p&gt;Give the incumbents their due first, because they earn it. Mockaroo will hand you a realistic CSV, JSON, or SQL file in under a minute with no install, and it doubles as a mock-API host (per mockaroo.com, as of July 2026). generatedata.com is open-source under the GPL and free to self-host, with roughly thirty data types and a stack of export formats (per the project's GitHub, benkeen/generatedata, as of July 2026). Beeceptor advertises 300-plus field generators and can turn what it produces into a live mock REST API on the spot (per beeceptor.com, as of July 2026). SingleStore ships a basic column-by-column dummy-data widget too (as of July 2026). For a quick single-table export or one faked API response, reaching past any of these would be overkill.&lt;/p&gt;

&lt;p&gt;The shared limit is structural, and it isn't a bug in any of them. Each generates one table at a time, with the columns of that table drawn independently. There is no live database in the loop, so nothing connects the value in one table to a value in another.&lt;/p&gt;

&lt;p&gt;Watch where that lands on a product. A SaaS demo usually needs &lt;code&gt;organizations&lt;/code&gt; that own &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;users&lt;/code&gt; who hold &lt;code&gt;subscriptions&lt;/code&gt;, &lt;code&gt;subscriptions&lt;/code&gt; that produce &lt;code&gt;invoices&lt;/code&gt;, and &lt;code&gt;invoices&lt;/code&gt; backed by &lt;code&gt;usage_events&lt;/code&gt;. Generate those five tables in five independent passes and the foreign keys are decorative. An &lt;code&gt;invoice.customer_id&lt;/code&gt; is a believable integer that happens to point at no customer you created. Every account ends up on the same subscription tier because nothing wired tier to account. Drill into a single record on screen and the illusion falls apart, which is precisely the interaction a demo is built to invite. The &lt;a href="https://seedfa.st/compare/mockaroo-alternative" rel="noopener noreferrer"&gt;Mockaroo alternative&lt;/a&gt; write-up walks through this exact wall in more depth, and the &lt;a href="https://seedfa.st/blog/test-data-generation" rel="noopener noreferrer"&gt;test data generation&lt;/a&gt; rundown covers the same tools from the CI-suite angle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspho5dhuubpu60dei1zb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspho5dhuubpu60dei1zb.webp" alt="psql output on flat-generated tables, where invoices joined to customers come back empty-handed and a LEFT JOIN count finds 174 orphaned invoices" width="800" height="688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;psql makes it countable — in the sample above, eight of the ten invoices bill customers that don't exist, and the orphan count across the whole table comes to 174.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a demo actually needs to look real
&lt;/h2&gt;

&lt;p&gt;Valid foreign keys are only the floor, and a demo that clears referential integrity can still look obviously synthetic the second someone with domain sense looks at it, because real data carries a &lt;em&gt;shape&lt;/em&gt; that random values don't.&lt;/p&gt;

&lt;p&gt;Picture the customer list you'd actually want on screen. It would hold a handful of large accounts with dozens of seats and long histories sitting above a long tail of small accounts that signed up last week and barely logged in — not fifty identical companies each with the same three users. Signup dates would land before the first invoice rather than after it, and invoice totals would add up from their line items instead of floating free. None of that is exotic; it's the difference between rows generated with awareness of each other and rows that weren't.&lt;/p&gt;

&lt;p&gt;This is the same coherence argument that &lt;a href="https://seedfa.st/blog/realistic-test-data" rel="noopener noreferrer"&gt;realistic test data&lt;/a&gt; makes for a test suite, turned here to face the person watching. A test only fails when incoherence trips a check it happens to run. The demo has a harsher reviewer in the human watching it, who spots the seams fast, because a customer that predates its own signup or an order with no buyer is exactly the thing a skeptical prospect clicks straight toward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating demo data against your own schema
&lt;/h2&gt;

&lt;p&gt;The way out is to stop generating tables in isolation and start generating from the schema that already defines how they relate. Point a tool at the staging or demo database, let it read the live schema — every table, column, type, and foreign key — and have it write a connected dataset across all of them in one run, with no flat files left to reconcile by hand afterward.&lt;/p&gt;

&lt;p&gt;That's the model Seedfast works in. It connects to your application's database, reads the current schema on every run, and generates rows that reference each other correctly, so an invoice lands on a customer who exists and a subscription belongs to an account that was created before it. You describe the demo you want in plain language and it fills the tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;seedfast seed &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"50 organizations with users, subscriptions, and 6 months of invoices"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fztbdz8cwh6cvbenc6z7o.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fztbdz8cwh6cvbenc6z7o.webp" alt="Seedfast CLI run seeding organizations, users, subscriptions, and invoices in one pass — 575 connected rows across four tables" width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's no form to fill column by column and no per-table export to stitch back together afterward. Because it reads the schema fresh each time, the migration you ship next week gets picked up on the next run, with no saved definition to quietly break. It runs the same way as a CLI step against a staging connection string or as an &lt;a href="https://seedfa.st/docs/mcp-setup-guide" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; inside an AI coding agent, so the agent building the demo can populate the database itself. There's a &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;free plan&lt;/a&gt; that takes no card if you want to point it at a real schema before deciding anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo data generator options compared
&lt;/h2&gt;

&lt;p&gt;The flat generators win the rows they were built to win — a single fast export, a hosted mock API — while a schema-aware tool takes over once those rows have to agree with each other.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Mockaroo&lt;/th&gt;
&lt;th&gt;generatedata.com&lt;/th&gt;
&lt;th&gt;Beeceptor&lt;/th&gt;
&lt;th&gt;Seedfast&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reads your live database schema&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes — on every run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keeps foreign keys valid across tables&lt;/td&gt;
&lt;td&gt;Manual — own web form only&lt;/td&gt;
&lt;td&gt;Not documented&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes — every child row points at a real parent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-table demo in one run&lt;/td&gt;
&lt;td&gt;No — one table at a time&lt;/td&gt;
&lt;td&gt;No — one table at a time&lt;/td&gt;
&lt;td&gt;No — one dataset at a time&lt;/td&gt;
&lt;td&gt;Yes — all connected tables together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs without a live database&lt;/td&gt;
&lt;td&gt;Yes — exports files&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No — needs a connection string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;CSV, JSON, SQL, Excel&lt;/td&gt;
&lt;td&gt;CSV, SQL, JSON, XML, and more&lt;/td&gt;
&lt;td&gt;JSON, CSV, SQL, plus a live mock API&lt;/td&gt;
&lt;td&gt;Rows written straight into your database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;Free ≤1,000 rows/file; paid $60–$7,500/yr&lt;/td&gt;
&lt;td&gt;Free, open-source (GPL-3.0)&lt;/td&gt;
&lt;td&gt;Free tier; no pricing on the tool page&lt;/td&gt;
&lt;td&gt;Free plan, then flat monthly credits, no per-row metering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Fastest single flat file or a mock API&lt;/td&gt;
&lt;td&gt;Free self-hosted flat exports&lt;/td&gt;
&lt;td&gt;Turning generated rows into a mock API&lt;/td&gt;
&lt;td&gt;Coherent multi-table demos against your own DB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Competitor facts verified as of July 2026: Mockaroo pricing per mockaroo.com/pricing; generatedata.com license and feature counts per the benkeen/generatedata GitHub repository; Beeceptor capabilities per beeceptor.com. In my read, all three are the right call when you need one flat table fast, though they become the wrong tool the moment a demo depends on tables referencing each other, which no amount of extra rows fixes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The quickest check is a run against your own staging schema — that's what the &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;free plan&lt;/a&gt; is for.&lt;/p&gt;

&lt;p&gt;If you're weighing Mockaroo specifically, the &lt;a href="https://seedfa.st/compare/mockaroo-alternative" rel="noopener noreferrer"&gt;Mockaroo alternative&lt;/a&gt; page has the full side-by-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits: trial accounts, staging, investor decks, and QA walkthroughs
&lt;/h2&gt;

&lt;p&gt;The "make it look alive" problem shows up in more places than a sales call. A self-serve trial that greets a new user with an empty dashboard converts worse than one opening on a populated workspace. Investors want enough history behind the charts for them to read as a story, and a QA walkthrough of a new feature lands better against data that resembles a real tenant than against three rows named "Test User". What all of these share is the need for a database that looks used, without a real customer's information anywhere in it.&lt;/p&gt;

&lt;p&gt;That last constraint is why a demo environment and a &lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;staging environment&lt;/a&gt; tend to converge. Both have to look real and neither should be a copy of production. Generating the data from the schema satisfies both at once. Nobody's personal data leaves production, because none of it was ever involved; what the generator does see is schema structure — table and column names — which a regulated team will want to review. And there's no anonymization script to keep in sync as the schema moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is demo data?
&lt;/h3&gt;

&lt;p&gt;Demo data is fabricated data you load into your own application's database so the product looks real to whoever is watching the screen. The bar is visual coherence: names that read like a genuine customer base, dates in a sensible order, and records that connect when someone drills into them — down to the invoice that resolves to a real customer.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between demo data and test data?
&lt;/h3&gt;

&lt;p&gt;Test data exists to exercise your code — edge cases, null fields, boundary values, the exact rows an assertion checks — and no one ever looks at it. Demo data exists to be looked at, so it has to hold together as a believable picture even though no test will read it. One generator can serve both, but they pull in different directions, and a dataset tuned to break a pagination bug rarely resembles a clean sales demo. If you actually wanted software that assembles the pitch itself, that's a sales-demo-automation platform like Reprise or Demostack — a separate, pricier category from a data generator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a demo data generator handle foreign keys between tables?
&lt;/h3&gt;

&lt;p&gt;Some can; most can't. The flat generators that dominate this space produce one table at a time, so a foreign key column fills up with plausible integers that don't necessarily match any row you generated in the parent table. A schema-aware generator — Seedfast is built this way — reads the live database first and writes rows that reference real parents, so an invoice belongs to a customer who exists and a subscription belongs to an account created before it. That difference decides whether a demo survives someone clicking into it or only survives a screenshot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Mockaroo good for generating demo data?
&lt;/h3&gt;

&lt;p&gt;For a single flat table, yes — it's the fastest path to a realistic CSV or SQL export, and for mocking one API response it's hard to beat. The boundary appears when a demo needs several tables to agree with each other — Mockaroo generates each table independently and can only relate tables you rebuild by hand in its own web form, not the ones already defined in your database (as of July 2026, per mockaroo.com), so a multi-table demo turns into exporting each file and wiring the keys yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I generate demo data for a SaaS product without using real customer data?
&lt;/h3&gt;

&lt;p&gt;Generate it from your schema instead of copying a slice of production. A schema-aware tool like Seedfast connects to a staging or demo database, reads the tables and their relationships, and writes a fresh set of organizations, users, subscriptions, and activity that never belonged to anyone — no export of real customers, no anonymization pass to maintain. Because production rows never leave production, there's no PII in the demo to leak. The generator still reads schema metadata like table and column names, so a regulated team should glance at what those names reveal first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it against your own schema
&lt;/h2&gt;

&lt;p&gt;If your demos keep springing leaks on the first click, the fix isn't more realistic-looking columns — it's data that knows how your tables connect. &lt;a href="https://seedfa.st/" rel="noopener noreferrer"&gt;Seedfast&lt;/a&gt; reads your live schema and writes a connected dataset across every related table, so the account you show someone holds together when they start clicking. The &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;free plan&lt;/a&gt; takes no card, and there's no table or seed ceiling on it to trip over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/compare/mockaroo-alternative" rel="noopener noreferrer"&gt;Mockaroo alternative&lt;/a&gt; — the full side-by-side when your data has foreign keys&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/realistic-test-data" rel="noopener noreferrer"&gt;Realistic test data&lt;/a&gt; — the coherence argument, aimed at a test suite&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/staging-without-prod-data" rel="noopener noreferrer"&gt;Staging without production data&lt;/a&gt; — the same schema-first approach for staging environments&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seedfa.st/blog/test-data-generation" rel="noopener noreferrer"&gt;Test data generation: 7 methods compared&lt;/a&gt; — the toolbox when you need CI data, not a demo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/demo-data-generator" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>testing</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
    <item>
      <title>MCP Servers for Test Data: What Exists and What Each One Does</title>
      <dc:creator>Mikhail Shytsko</dc:creator>
      <pubDate>Sat, 18 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikh-shytsko/mcp-servers-for-test-data-what-exists-and-what-each-one-does-30ol</link>
      <guid>https://dev.to/mikh-shytsko/mcp-servers-for-test-data-what-exists-and-what-each-one-does-30ol</guid>
      <description>&lt;p&gt;Your coding agent is connected, the migrations have run, and the database behind them is empty. Ask around for an MCP server to fix that and the shelf looks crowded — mcpservers.org lists more than 870 in its database category alone (&lt;a href="https://mcpservers.org/category/database" rel="noopener noreferrer"&gt;mcpservers.org/category/database&lt;/a&gt;) — but almost none of them do the job you're picturing. They hand the agent a way to run SQL against data that already exists. What you actually want, MCP test data, is the opposite errand, a server that puts realistic, connected rows into a schema that currently holds none.&lt;/p&gt;

&lt;p&gt;This page surveys the servers that do that, and the much larger category that doesn't, so you can tell one from the other before wiring anything into an agent that can write to your database. It reflects a landscape where the "which MCP gives my agent test data" shelf is still surprisingly thin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access or generation? The line that splits the category
&lt;/h2&gt;

&lt;p&gt;Here is the distinction that decides everything downstream. A database-access MCP lets an agent run SQL against data that already exists — it inspects tables, reads schemas, executes queries, and sometimes writes individual rows. A test-data MCP generates the realistic, relationally consistent data in the first place and writes it into the schema. Against an empty database the access kind is useless, since there's nothing there to query, and the generation kind is what fills it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fseedfa.st%2Fblog%2Fmcp-test-data-2.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fseedfa.st%2Fblog%2Fmcp-test-data-2.svg" alt="Two lanes of MCP server — the database-access kind that queries rows that already exist and idles on an empty schema, and the test-data kind that generates connected rows and fills it" width="960" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most of those 870-plus database servers are the first kind. Point one at a fresh container or a just-migrated branch and it sits idle, because a query needs rows to return and there aren't any. You could skip the MCP entirely and ask the agent to write the &lt;code&gt;INSERT&lt;/code&gt; statements itself, which works right up until the foreign keys have to line up across a dozen tables — the point where model-written seed SQL tends to come apart, walked through in the &lt;a href="https://seedfa.st/blog/generate-test-data-with-ai" rel="noopener noreferrer"&gt;generate test data with AI&lt;/a&gt; playbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP test data servers at a glance
&lt;/h2&gt;

&lt;p&gt;Four options cover almost everyone reading this, and the table below sorts them by what they actually put in your database, not by how they market it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MCP server&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Pricing model&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Seedfast MCP&lt;/td&gt;
&lt;td&gt;Generates connected, realistic rows and writes them into your own database in place, from one &lt;code&gt;seedfast_run&lt;/code&gt; call&lt;/td&gt;
&lt;td&gt;Monthly credits (free plan, then $16 / $69), no per-token meter&lt;/td&gt;
&lt;td&gt;Seeding your own Postgres, Supabase, or Neon from an agent or CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tonic Fabricate MCP&lt;/td&gt;
&lt;td&gt;Bridges Claude or Cursor to Fabricate's hosted generation agent, which synthesizes data across several engines&lt;/td&gt;
&lt;td&gt;Plan + credit/token metering (Plus $29/mo, as of July 2026)&lt;/td&gt;
&lt;td&gt;Interactive, multi-engine generation from the editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SeedBase Test Data (OSS)&lt;/td&gt;
&lt;td&gt;FK-consistent data generator, open source&lt;/td&gt;
&lt;td&gt;Free (open source)&lt;/td&gt;
&lt;td&gt;Self-hosted setups and OSS-only shops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database-access MCPs (Oracle, Microsoft SQL, Cloudflare D1, DBmaestro, Salesforce)&lt;/td&gt;
&lt;td&gt;Let an agent query and administer data that already exists; do not generate it&lt;/td&gt;
&lt;td&gt;Free or bundled with the platform&lt;/td&gt;
&lt;td&gt;Inspecting, querying, or operating a database that already has rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Generation lives in the top two rows. Everything below only reads data that already exists, which is why that one split matters more than any feature comparison you could run inside either camp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seedfast MCP seeds your database in place
&lt;/h2&gt;

&lt;p&gt;Seedfast runs as an MCP server that a coding agent launches over the Model Context Protocol and then calls with a single tool, &lt;code&gt;seedfast_run&lt;/code&gt;. You describe the data you need in plain language, it reads your live schema, and it writes realistic rows into the database you're already connected to, with the foreign keys resolving so the rows genuinely hang together rather than pointing at records that were never created. Nothing gets shipped out to a hosted service and loaded back afterward; the data lands directly in your own Postgres, whether that's a Supabase branch, a Neon branch, an RDS instance, or a throwaway container spun up for a test run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp3drfy4ba28axngo0rjv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp3drfy4ba28axngo0rjv.webp" alt="Claude Code calling the seedfast_run MCP tool with a plain-language scope and getting back 213 connected rows across users, projects, tasks, and comments" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What sets its KIND of MCP apart from the rest of this list is where the work actually happens — it seeds in place, so the database you connect is the one that fills. Pricing stays flat too, a monthly plan with a free tier under it and no per-token meter so a CI seed costs the same as one you run by hand, and because it's CLI-first, the same call the agent makes also drops into a pipeline as a plain command after your migrations. The client config is the same &lt;code&gt;npx&lt;/code&gt; block across Claude Code, Cursor, and VS Code; the &lt;a href="https://seedfa.st/docs/mcp-setup-guide" rel="noopener noreferrer"&gt;MCP setup guide&lt;/a&gt; carries the per-client version, with focused walkthroughs for &lt;a href="https://seedfa.st/blog/claude-code-mcp-database-seeding" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; and &lt;a href="https://seedfa.st/blog/antigravity-mcp-database-seeding" rel="noopener noreferrer"&gt;Google Antigravity&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tonic Fabricate's MCP bridges to a hosted agent
&lt;/h2&gt;

&lt;p&gt;Tonic launched a Fabricate MCP on July 1, 2026, and it is a real, capable server. Connect Claude or Cursor to it and you can generate synthetic data without leaving the editor, the same conversational flow Fabricate offers in the browser. Where it differs from an in-place seeder is the location of the work. The MCP hands your request to Fabricate's hosted generation agent, which is metered by credit and token, and the generated data comes back to be loaded, rather than the seed running against the database you're connected to.&lt;/p&gt;

&lt;p&gt;Fabricate's reach is the genuine draw. It connects live databases and generates into Postgres, MySQL, Oracle, and Databricks, with exports to formats an in-place Postgres seeder doesn't touch. If your work spans several engines, that breadth is a real reason to pick it. The cost model is the trade to weigh against it. Generation is metered — a free tier carrying $5/month in credits for a personal email or $10/month with every model unlocked for a work email, a Plus plan at $29/month including $25 in credits, then pay-as-you-go around $0.17 a standard turn (&lt;a href="https://www.tonic.ai/pricing" rel="noopener noreferrer"&gt;tonic.ai/pricing&lt;/a&gt;; figures as of July 2026, worth re-checking before you budget). One practical note for anyone hunting the registries — as of July 2026 the Fabricate MCP wasn't listed on Glama, mcp.so, or Smithery, so you add it from Tonic's own docs rather than a one-click install. The longer head-to-head lives in &lt;a href="https://seedfa.st/compare/seedfast-vs-tonic-fabricate" rel="noopener noreferrer"&gt;Seedfast vs Tonic Fabricate&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open-source and community servers
&lt;/h2&gt;

&lt;p&gt;Both registries carry a long tail of community-built servers. Glama alone scans tens of thousands (&lt;a href="https://glama.ai/mcp/servers" rel="noopener noreferrer"&gt;glama.ai/mcp/servers&lt;/a&gt;) and groups the relevant ones under a Databases category, while mcpservers.org keeps a parallel list. Most of what you'll find there is the access kind, though a smaller number aim at generation — SeedBase Test Data, listed on Glama, describes itself as generating FK-consistent synthetic data for a database from an AI assistant (&lt;a href="https://glama.ai/mcp/categories/databases" rel="noopener noreferrer"&gt;glama.ai/mcp/categories/databases&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For anyone who wants something self-hosted and open-source, the registries are the place to start. Two cautions travel with that choice, though. Quality and upkeep swing widely across a directory that size, so the scan score and the date of the last commit tell you more than the description does. And an agent with a write path into your database deserves the same scrutiny you'd give any dependency, community server or not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger category is database-access MCPs
&lt;/h2&gt;

&lt;p&gt;The reason the shelf looks so full is that nearly every major data platform now ships an MCP, and they are overwhelmingly access servers. Oracle exposes its database to agents through SQLcl (&lt;a href="https://www.oracle.com/mcp/" rel="noopener noreferrer"&gt;oracle.com/mcp&lt;/a&gt;). Microsoft's SQL MCP Server lets an agent work with tables, views, and stored procedures (&lt;a href="https://learn.microsoft.com/en-us/azure/data-api-builder/mcp/overview" rel="noopener noreferrer"&gt;Microsoft Learn&lt;/a&gt;). From Cloudflare, the Bindings MCP reaches D1 databases along with the rest of the Workers stack (&lt;a href="https://blog.cloudflare.com/thirteen-new-mcp-servers-from-cloudflare/" rel="noopener noreferrer"&gt;blog.cloudflare.com&lt;/a&gt;). DBmaestro's, announced in April 2026, puts release automation and CI/CD orchestration for database pipelines under natural-language control (&lt;a href="https://www.infoq.com/news/2026/04/dbmaestro-mcp-server/" rel="noopener noreferrer"&gt;InfoQ&lt;/a&gt;), while Salesforce's surfaces org data and metadata to an agent.&lt;/p&gt;

&lt;p&gt;Every one of these earns its place, and not one of them generates test data. They let an agent read, query, or operate a database whose rows are already there. Aim any of them at an empty schema and it has nothing to act on; a generation server is what changes that. Plenty of teams keep one registered alongside an access server, because the day you spin up a blank schema is exactly when the query tools have nothing to say.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which MCP server should you wire up?
&lt;/h2&gt;

&lt;p&gt;Start from what you actually have in front of you. If the database already holds data and you want the agent to inspect it, query it, or run migrations against it, one of the access MCPs above is the whole answer, and bolting on a generator you'll never call is just clutter in the config.&lt;/p&gt;

&lt;p&gt;If the schema is empty — a fresh container, a branch cloned thin, a demo environment with no rows — you need a generation server, and there the choice turns on interface and cost. For in-place seeding of your own Postgres from an agent or a CI step, on flat pricing, Seedfast is the one I reach for, and since I work on it you should read that as a stated interest rather than a neutral ruling. Fabricate's breadth is hard to beat when you're generating interactively across several database engines from a browser or editor. And if you'd rather vet a self-hosted option yourself, the open-source servers on the registries are worth a look. If you're cross-shopping beyond MCP servers specifically, the &lt;a href="https://seedfa.st/blog/database-seeder" rel="noopener noreferrer"&gt;best database seeding tool&lt;/a&gt; roundup covers ORM seeders and standalone generators side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an MCP server for test data?
&lt;/h3&gt;

&lt;p&gt;A test-data MCP server is one whose tools generate realistic, relationally consistent rows and write them into a database, so an AI coding agent can populate an empty schema by describing what it needs in plain language. It's distinct from the far more common database-access MCP, which only reads or queries data that already exists. Seedfast is an example of the generating kind; the Oracle, Microsoft SQL, and Cloudflare D1 servers are examples of the access kind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there an MCP server that generates data instead of just querying it?
&lt;/h3&gt;

&lt;p&gt;Yes, a few. Seedfast generates connected rows and writes them into your own database in place. Tonic's Fabricate MCP generates through a hosted, credit-metered agent. And a handful of open-source servers, such as SeedBase Test Data on Glama, aim at the same job for self-hosted setups. Most database MCP servers, by contrast, only run SQL against data that is already there, which does nothing for an empty schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Tonic Fabricate have an MCP server?
&lt;/h3&gt;

&lt;p&gt;It does. Tonic launched a Fabricate MCP on July 1, 2026, and connecting Claude or Cursor to it lets you generate synthetic data from the editor. The request runs through Fabricate's hosted generation agent, metered by credit and token (&lt;a href="https://www.tonic.ai/pricing" rel="noopener noreferrer"&gt;tonic.ai/pricing&lt;/a&gt;), rather than seeding your connected database directly. Registry listings haven't caught up yet, so Tonic's own docs are still where you install it from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are there open-source MCP servers for test data?
&lt;/h3&gt;

&lt;p&gt;There are. The Glama and mcpservers.org registries list community-built servers under their database categories, and a few of them, SeedBase Test Data among them, sit on the generating side of the split this page draws rather than the querying side (&lt;a href="https://glama.ai/mcp/categories/databases" rel="noopener noreferrer"&gt;glama.ai&lt;/a&gt;). Coverage and maintenance vary a lot across a registry that large, so check each server's scan score and recent commit history before you connect it to a database it can write to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which MCP server is best for database seeding?
&lt;/h3&gt;

&lt;p&gt;The best MCP server for database seeding depends on where your database lives and how you pay. For seeding your own Postgres in place from an agent or a CI pipeline on a flat plan, Seedfast fits that shape (a stated interest — I maintain it). For generating across multiple engines from a browser or editor, Tonic Fabricate's reach is the stronger fit despite the metered cost. If you want to inspect or query an existing database rather than fill an empty one, you don't want a seeding server at all — an access MCP does that job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Strip the 870-server shelf down and it sorts into two piles. Almost everything is an access MCP that reads a database already holding rows. A handful generate the rows instead — Seedfast in place on your own database, Fabricate through its hosted agent, a few open-source servers if you'd rather self-host and vet the code. If the problem you keep hitting is the empty schema an agent stalls on, &lt;a href="https://seedfa.st/pricing" rel="noopener noreferrer"&gt;Seedfast's free plan&lt;/a&gt; is enough to register one config block and watch a database fill from a single prompt, without a card and without any production data touching the run.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://seedfa.st/blog/mcp-test-data" rel="noopener noreferrer"&gt;seedfa.st&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>database</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
