Data that lives in your code
Almost every app I've worked on has data that isn't created by users. It ships with the code:
-
Roles and permissions:
admin,editor,viewer. - Categories behind a multi-select.
- Location data: countries and their states or provinces.
Your app depends on these rows existing. They have to be the same in development, staging and production. And they change over time: a new role, a renamed category, a country you didn't support last year.
Prisma handles your schema with migrations. But what about the rows inside it?
The problem with one seed script
Prisma's recommended approach is a single seed script that prisma db seed runs. It looks fine on day one:
// prisma/seed.ts
async function main() {
await db.role.createMany({ data: roles })
await db.category.createMany({ data: categories })
await db.country.createMany({ data: countries })
await db.state.createMany({ data: states })
// ...and everything you add next month
}
main()
Then the data starts to change, and the script runs into problems:
- It runs everything, every time. Running it twice either fails on duplicates or forces you to make every line idempotent.
- It doesn't know what has already run. There's no record of which data your production database already has.
- You can't undo one piece. Removing a category you seeded by mistake means writing a one-off script.
- Changing data is awkward. You either reset the whole database or hand-write updates somewhere else.
- It only grows. Roles, categories and locations all end up in one file with no history.
It works, but it isn't elegant. And it gets harder to trust the longer the project lives.
I'd already solved this for schemas
I never let my seed script get out of hand. Before Prisma, I had used migration libraries in other projects. They solve exactly this problem for schema changes:
- Each change lives in its own timestamped file.
- Each file has an
upfunction to apply it and adownfunction to revert it. - A table records which files have run, so each one runs only once.
So when I started a new project with Prisma, I went looking for the same thing for seeds. I couldn't find a library that did it. So I built one.
Meet prisma-seeder
prisma-seeder brings migration-style seeds to Prisma. Each seed is a file with an up and a down function that receive your Prisma client.
-
Runs each seed once: applied seeds are recorded in a
_prisma_seedstable. -
Reverts seeds with
down, latest first or by name. -
Re-runs seeds after you change their data, with
refreshorreset. - Generates seed files with a timestamp, so they always run in order.
-
Typed seeds:
dbknows your models, with autocomplete. - Works with Prisma ORM 4 to 8.
I've used it in my own project since the first release.
Seeding roles with Prisma 7
Let's seed a list of roles. The example uses Prisma 7 with PostgreSQL. The README has the setup for Prisma 4, 5, 6 and 8.
1. Install
npm install prisma-seeder
npm install --save-dev tsx
The examples use tsx to run TypeScript. If you use Bun, swap npx tsx for bun in the commands below and in the seed line of prisma.config.ts.
2. Add the table that tracks applied seeds
Add the PrismaSeeds model next to your Role model. @@ignore keeps it out of your Prisma Client.
// prisma/schema.prisma
model Role {
id String @id @default(uuid())
name String @unique
}
model PrismaSeeds {
name String @id()
@@map("_prisma_seeds")
@@ignore
}
npx prisma migrate dev
npx prisma generate
3. Create the seed script
The seed script is a small CLI. The Register declaration gives your seeds your client's type.
// prisma/seed.ts
import { PrismaPg } from '@prisma/adapter-pg'
import path from 'node:path'
import { PrismaSeeder } from 'prisma-seeder'
import { PrismaClient } from '../generated/prisma/client.js'
const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL! }) })
declare module 'prisma-seeder' {
interface Register {
client: typeof db
}
}
const seeder = new PrismaSeeder({
client: db,
seeds: path.join(import.meta.dirname, './seeds/*.{ts,js}')
})
await seeder.run()
process.exit()
This script uses top-level await and import.meta.dirname, so your project should be ESM: add "type": "module" to your package.json.
Register it in prisma.config.ts, so prisma db seed applies your pending seeds:
// prisma.config.ts
import 'dotenv/config'
import { defineConfig } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
seed: 'tsx prisma/seed.ts up'
},
datasource: {
url: process.env.DATABASE_URL!
}
})
4. Write the data with fixed UUIDs
This is the most important part. Generate each id once (for example with crypto.randomUUID() or uuidgen) and write it into the data file. A fixed id means the seed always targets the same rows, so you can run it again safely.
Keep data files out of the folder your glob matches, for example in a data subfolder:
// prisma/seeds/data/roles.ts
export const roles = [
{ id: '0f8b6c1e-3a52-4d7e-9b1a-6c2f4e8d9a01', name: 'admin' },
{ id: '5d2a9e47-81c3-4b6f-a0d8-3e7c1b9f2a64', name: 'editor' },
{ id: 'b93c4f10-6e2d-47a8-8c5b-1f0a7d3e6b29', name: 'viewer' }
]
5. Generate and write the seed
npx tsx prisma/seed.ts generate --name add_roles
This creates prisma/seeds/20261011120000_add_roles.ts. Fill in up and down:
import type { Seed } from 'prisma-seeder'
import { roles } from './data/roles.js'
export const up: Seed = async (db) => {
// All roles are saved, or none are
await db.$transaction(
roles.map((role) =>
// upsert lets refresh run this seed again with updated data
db.role.upsert({
where: { id: role.id },
create: role,
update: { name: role.name }
})
)
)
}
export const down: Seed = async (db) => {
await db.role.deleteMany({
where: { id: { in: roles.map((role) => role.id) } }
})
}
6. Apply it
npx prisma db seed
Run it again and nothing happens: the seed is already recorded in _prisma_seeds.
7. Change the data
Say viewer should be called reader. Edit the data file, then re-run only that seed:
npx tsx prisma/seed.ts refresh --name 20261011120000_add_roles
The fixed id finds the existing row, and upsert renames it. No database reset needed. To run down first and then up, use reset instead of refresh.
For brand-new data, such as a new set of categories, generate a new seed. prisma db seed applies only that one.
8. Revert it
# Revert the last applied seed
npx tsx prisma/seed.ts down
# Or a specific one
npx tsx prisma/seed.ts down --name 20261011120000_add_roles
A few more things
Seeds per environment. Roles belong everywhere. Fake users belong only in tests. Put them in separate folders with one seed script each, such as seeds/*.ts and seeds/test/*.ts. Each script only sees and reverts its own seeds. The README walks through it.
Custom templates. The template option sets what generate writes, if your team has its own seed boilerplate.
Limitations, honestly:
- You add the
PrismaSeedsmodel to your schema yourself. If prisma-seeder created the table on its own, Prisma would see it as drift and offer to reset your database. - MongoDB isn't supported.
- Prisma 8 has no
prisma db seed, so there you run seeds with an npm script instead.
Try it
If your seed script handles roles, categories or locations and you're tired of resetting the database to change one row, give it a try:
- GitHub: esmerlingb/prisma-seeder
- npm: prisma-seeder
npm install prisma-seeder
If it helps you, a star on GitHub helps other developers find it. Bug reports, ideas and pull requests are welcome.
How do you handle reference data in your Prisma projects today? Tell me in the comments.
Top comments (1)
I split it the same way: reference data (roles, countries) lives in versioned seeds like yours, and anything relational and fake is generated per environment from a fixed random seed so it is reproducible but never committed. The part that usually bites is the fake data pointing at reference rows that a down() just removed. Do you check foreign keys after a revert, or rely on the database to complain?
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more