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 data

Migration 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:generate

This 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:migrate

Prisma will prompt you for a migration name (e.g. add_project_model). It then:

  1. Diffs the current schema against the last migration state
  2. Generates a migration.sql file in prisma/migrations/
  3. 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:push

When 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:migrate

When 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_name

If 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 reset

prisma 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 pgvector
CREATE 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?

Edit on GitHub

Last updated on

On this page