# Running Prisma migrations on Vercel without breaking production

> Put prisma generate and prisma migrate deploy in the Vercel build command, never db push or migrate dev, and design migrations to survive a rolling deploy.

You deploy to Vercel and the site 500s with:

```
PrismaClientKnownRequestError:
The table `public.project` does not exist in the current database.
```

The table exists locally. The migration is committed. The build succeeded.
Nothing applied it, because nothing was ever told to.

Vercel builds your code. It does not know your database exists.

## Where migrations belong

Run them in the **build command**, before `next build`:

```
bun run db:generate && bun run db:migrate:deploy && bun run build
```

Set that in Vercel under Settings → Build & Development Settings → Build
Command (override it), or in `vercel.json`:

```json
{
  "buildCommand": "bun run db:generate && bun run db:migrate:deploy && bun run build"
}
```

Three commands, in this order, for three reasons:

**`db:generate`** (`prisma generate`) because the client is generated code that
is not in git. Prisma 7 writes it to the folder the generator block names (in
this repo `src/generated/prisma`, which `.gitignore` lists). This repo also runs
generate from `postinstall`, which covers every clone and every cold CI job,
but an install that is a no-op on a cache hit may not run lifecycle scripts at
all. Then the build either cannot find the client or typechecks against one
generated from an older schema, so the build command says it out loud.

**`db:migrate:deploy`** (`prisma migrate deploy`) because it is the only
migration command that is safe to run unattended. It applies pending migrations
in order and stops on the first failure. It never prompts, never resets, never
generates a new migration.

**`build` last** so a failed migration fails the deploy before any new code
goes live.

## The commands that must never run here

- **`prisma migrate dev`** compares the schema to a shadow database and can
  offer to reset. Unattended, against production, that is catastrophic. It is a
  development command, full stop.
- **`prisma db push`** applies the schema with no migration history. It
  resolves differences by dropping columns and tables, silently. It is for a
  local scratch database while you are still shaping a model.
- **Manual `psql` DDL** puts the database out of step with
  `_prisma_migrations`, and the next `migrate deploy` reports drift.

## Migrations must not need `DATABASE_URL` to be the pooled one

`migrate deploy` connects with the URL in `prisma.config.ts`, never the one the
app's driver adapter uses, so the two can differ:

```ts
// prisma.config.ts
export default defineConfig({
  schema: "prisma/schema.prisma",
  datasource: { url: process.env.DIRECT_URL }, // direct, unpooled
});
```

Prisma 7 loads no `.env` file by itself, and Vercel does not need one: it sets
the variables in the build environment. On your laptop, `prisma.config.ts` has
to load `.env.local` itself; this repo's does.

The direct string must exist in Vercel for **every** environment: Production,
Preview and Development. The most common "it works in production but every
preview deploy fails" cause is a direct URL that was only added to Production.
A migration through a transaction pooler hangs on an advisory lock rather than
failing cleanly, so the error you get is a timeout with no useful detail. (This
repo's config falls back to rewriting the pooled Neon or Supabase host when the
direct string is missing, which saves you on those two providers and nowhere
else.)

## Design migrations for a rolling deploy

Even with the ordering above, there is a window (usually seconds, sometimes
longer) where the migration has applied and old instances are still serving
traffic with the old code. Any migration that removes or renames something
breaks those instances.

The rule is **expand, then contract**, across two deploys.

Renaming `name` to `fullName`:

*Deploy 1: expand.* Add `fullName` as nullable. Backfill it. Ship code that
writes both columns and reads `fullName ?? name`.

```sql
ALTER TABLE "user" ADD COLUMN "full_name" TEXT;
UPDATE "user" SET "full_name" = "name" WHERE "full_name" IS NULL;
```

*Deploy 2: contract.* Once no running code reads `name`, drop it and add the
constraint.

```sql
ALTER TABLE "user" ALTER COLUMN "full_name" SET NOT NULL;
ALTER TABLE "user" DROP COLUMN "name";
```

The same shape applies to adding a required column (add nullable → backfill →
set not null), and to narrowing a type. Prisma writes single-step SQL by
default because it cannot know your deploy strategy: split it yourself with:

```bash
bunx prisma migrate dev --create-only
```

which writes the migration file and lets you edit the SQL before it is applied.

## Long migrations and the build timeout

`migrate deploy` runs inside the build, and builds have a time limit. A
`CREATE INDEX` on a large table, or a backfill of millions of rows, will hit
it, and a build killed mid-migration leaves a failed row in
`_prisma_migrations` that blocks every later deploy.

For those, do the work out of band:

- Build indexes with `CREATE INDEX CONCURRENTLY` in a manually-run migration
  (it cannot run inside a transaction, so it needs its own migration file and a
  deliberate execution), then mark it applied with
  `bunx prisma migrate resolve --applied <name>`.
- Backfill in batches from a script you run yourself, not from a migration.

## Recovering from a failed deploy migration

`prisma migrate deploy` stops at the first failure and records it. Every
subsequent deploy then fails immediately with "migration failed to apply".

```bash
bunx prisma migrate status
```

Read what it says, fix the SQL, then tell Prisma what actually happened to the
failed migration:

```bash
# the migration's changes are NOT in the database
bunx prisma migrate resolve --rolled-back 20260401120000_add_project

# you applied the changes by hand and they ARE in the database
bunx prisma migrate resolve --applied 20260401120000_add_project
```

Then redeploy. Never delete rows from `_prisma_migrations` to make the error go
away: you are deleting the record of what your database contains.

## Preview deployments

Preview branches sharing the production database is the default and it is a
trap: a preview build applies migrations to production. Either point previews
at a branch database (Neon and Supabase both create one per branch) or drop
`migrate deploy` from preview builds and let production be the only thing that
migrates.

## The checklist

- Build command is `generate && migrate deploy && build`.
- `DATABASE_URL` (pooled) and the direct string (`DIRECT_URL`, or
  `DATABASE_URL_UNPOOLED` on Neon) set in all three Vercel environments.
- `prisma/migrations/**` committed alongside the schema change.
- Destructive changes split into expand and contract deploys.
- `bunx prisma migrate status` clean after the deploy.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
