Schema & Migrations
How to safely evolve the Prisma schema and run database migrations in development, CI, and production.
The Prisma schema is the single source of truth for the database structure. All changes to tables, columns, indexes, and relations go through packages/platform/db/prisma/schema.prisma. Migrations are versioned SQL files generated by Prisma and stored alongside the schema.
File locations
packages/platform/db/
prisma/
schema.prisma ← edit this to change the data model
migrations/ ← generated migration files (commit these)
20240101000000_init/
migration.sql
20240215120000_add_projects/
migration.sql
seed.ts ← development seed dataMigration files in prisma/migrations/ are the authoritative record of every schema change. Always commit them to version control — they are how production databases are kept in sync.
Development workflow
Open packages/platform/db/prisma/schema.prisma and make your changes — add a model, add a column, create an index, etc.
model Project {
id String @id @default(cuid())
name String
slug String @unique
description String?
tenantId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([tenantId])
@@map("projects")
}After editing the schema, regenerate the TypeScript client so your application code reflects the new types immediately.
pnpm db:generateThis runs prisma generate and updates the types in node_modules/@prisma/client. You do not need to restart the dev server — Next.js picks up the new types on the next request.
Create a named migration file and apply it to your local database in one command:
pnpm db:migratePrisma will prompt you for a migration name (e.g. add_project_model). It then:
- Diffs the current schema against the last migration state
- Generates a
migration.sqlfile inprisma/migrations/ - Applies the SQL to your local database
The generated migration file must be committed alongside the schema change. This is how CI/CD and production databases receive the update.
git add packages/platform/db/prisma/
git commit -m "feat(db): add Project model"Push vs migrate
pnpm db:push syncs your schema directly to the database without creating a migration file. Use it only for rapid local prototyping where migration history does not matter yet.
pnpm db:pushWhen to use:
- Early-stage exploration of a new model
- Throwaway local databases
- Iterating before you are ready to write a proper migration
db:push does not create a migration file. Changes applied this way cannot be replayed on other environments. Never use it on a shared, staging, or production database.
pnpm db:migrate creates a versioned migration file and applies it. This is the correct workflow for all environments beyond a throwaway local database.
pnpm db:migrateWhen to use:
- Adding or removing models in development
- Any change that needs to reach staging or production
- All changes in a team environment
The migration name is embedded in the directory name and used in audit logs.
CI/CD: running migrations before deploy
Migrations must be applied to the database before the new application code is deployed. Add this step to your pipeline:
- name: Run database migrations
run: pnpm db:migrate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
DIRECT_URL: ${{ secrets.DIRECT_URL }}Prisma requires DIRECT_URL (a direct, non-pooled connection) when running migrations. Connection poolers like PgBouncer and Supavisor do not support the DDL statements Prisma uses during migration. Set DIRECT_URL in your CI secrets and in schema.prisma:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}Rolling migrations for zero downtime
For production changes that affect large tables, use the expand-contract pattern to avoid locking rows during deployment:
model User {
// existing fields ...
displayName String? // ← nullable; old code ignores it, new code writes it
}Deploy this migration. Both old and new application versions run safely.
Run a background job or one-off script to populate displayName for all existing users. Keep it batched to avoid long-running transactions.
Once all rows are backfilled and only the new application version is live, apply a second migration to add the NOT NULL constraint.
model User {
displayName String // ← now required
}Reverting a migration
Prisma does not provide automatic rollback. To revert a migration:
Create a new migration that undoes the changes — drop the added column, recreate the dropped table, etc.
pnpm db:migrate
# Name it: revert_add_display_nameIf you are still in development and the migration has not reached production, you can delete the migration directory and reset the local database:
# Delete the unwanted migration directory
rm -rf packages/platform/db/prisma/migrations/20240215_add_display_name
# Reset local DB to the last good migration state
pnpm --filter @nebutra/db exec prisma migrate resetprisma migrate reset drops and recreates the entire local database. Never run it against a shared or production database.
pgvector setup
The recsys schema uses the pgvector extension. Your database must have it enabled before the initial migration runs.
pgvector is pre-installed on all Neon databases. No action required.
pgvector is available as a managed extension. Enable it from the Supabase dashboard under Database → Extensions, or via SQL:
CREATE EXTENSION IF NOT EXISTS vector;Install the extension on the PostgreSQL server, then enable it in your database:
# Debian/Ubuntu
sudo apt install postgresql-16-pgvector
# macOS (Homebrew)
brew install pgvectorCREATE EXTENSION IF NOT EXISTS vector;The first migration will fail with extension "vector" does not exist if pgvector is not installed before pnpm db:migrate is run for the first time.
How is this guide?
Last updated on