Prisma ORM 8 is here.Read the docs

Prisma ORM 7 to 8 (PostgreSQL)

Migrate a PostgreSQL project from Prisma ORM 7 to Prisma ORM 8 incrementally, with both versions running side by side

This guide is for teams running a Prisma ORM 7 application on PostgreSQL who want to move to Prisma ORM 8 without a rewrite. You will install Prisma ORM 8 next to Prisma ORM 7 in the same application, move routes over one at a time, hand migration ownership to Prisma ORM 8, and remove Prisma ORM 7 once nothing depends on it.

Both versions run against the same PostgreSQL database the whole time. The database, its data, and its connection string do not change; only application code and tooling do. Because each route stays on Prisma ORM 7 until you deliberately move it, the application remains shippable at every point in the migration.

The guide covers PostgreSQL only. Guidance for other databases will follow. If you are coming from v6 on MongoDB, see the MongoDB guide.

How the incremental migration works

The migration runs in five phases. The application works at the end of each one.

  1. Prepare Prisma ORM 7 for side-by-side operation. Move Prisma ORM 7 onto its own package name, binary, and config file. No behavior changes.
  2. Add Prisma ORM 8. Install the Prisma ORM 8 CLI and runtime with their own config, schema contract, and generated client. No application code uses them yet.
  3. Migrate one route. One route runs on Prisma ORM 8 while the rest stay on Prisma ORM 7, all against the same database.
  4. Transfer migration ownership. Prisma ORM 8 takes over planning and applying schema changes.
  5. Remove Prisma ORM 7 once nothing imports it.

The ownership timeline matters more than the code timeline. Prisma ORM 7 owns schema migrations through phases 1 to 3, and routes move to Prisma ORM 8 independently of that. Prisma ORM 8 takes over migrations only in phase 4, after a baseline migration, a database signature, and a ref are in place. You can pause between phases for as long as you need.

The prisma8-and-7-example repository shows the finished result of each phase (tags step-0 through step-3).

Prerequisites

  • Node.js 22.18 or newer (on the 24 line, 24.11 or newer); Node.js 24 recommended
  • A working Prisma ORM 7 application on PostgreSQL: prisma.config.ts, the prisma-client generator, and a driver adapter
  • TypeScript 5.3+ with "strict": true and a module setting that supports import attributes, such as "nodenext"

1. Prepare Prisma ORM 7 for side-by-side operation

Prisma ORM 8 expects the prisma package name, the prisma binary, and the prisma.config.ts file name. In this phase you move Prisma ORM 7 off those three names so Prisma ORM 8 can take them without ambiguity. Nothing migrates yet.

1.1. Confirm the application works

The guide follows a small Hono API with two routes. Map the file names to your own project. The Prisma ORM 7 pieces that matter:

package.json (excerpt)
{
  "scripts": {
    "prisma:generate": "prisma generate",
    "db:migrate": "prisma migrate dev"
  },
  "dependencies": {
    "@prisma/adapter-pg": "^7.10.0",
    "@prisma/client": "^7.10.0"
  },
  "devDependencies": {
    "prisma": "^7.10.0"
  }
}
prisma/schema.prisma
generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  published Boolean @default(false)
  authorId  Int
  author    User    @relation(fields: [authorId], references: [id], onDelete: Cascade)

  @@index([authorId])
}
prisma.config.ts
import "dotenv/config";
import { defineConfig } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: process.env["DATABASE_URL"],
  },
});
src/db.ts
import "dotenv/config";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../generated/prisma/client.js";

const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });

export const prisma = new PrismaClient({ adapter });

Two routes read and write through this client, /users and /posts:

src/routes/users.ts
import { Hono } from "hono";
import { prisma } from "../db.js";

export const users = new Hono();

users.get("/", async (c) => {
  const result = await prisma.user.findMany({
    include: { posts: true },
    orderBy: { id: "asc" },
  });
  return c.json(result);
});

users.post("/", async (c) => {
  const body = await c.req.json<{ email: string; name?: string }>();
  const user = await prisma.user.create({ data: body });
  return c.json(user, 201);
});

src/routes/posts.ts follows the same pattern for Post.

Start the app and run a read and a write:

bun run dev
curl -X POST localhost:3000/users -H 'content-type: application/json' \
  -d '{"email":"alice@prisma.io","name":"Alice"}'
curl localhost:3000/users

Do not continue until both requests succeed. That confirms the Prisma ORM 7 application works before you change its configuration.

1.2. Replace the prisma package with @prisma/prisma7

bun remove prisma
bun add --dev @prisma/prisma7@7.10.0-dev.58

@prisma/prisma7 is the same Prisma ORM 7 CLI under a version-specific name. It exposes a prisma7 binary and keeps prisma 7 as a transitive dependency. Your @prisma/client and @prisma/adapter-pg dependencies stay untouched.

1.3. Rename the Prisma ORM 7 config

mv prisma.config.ts prisma7.config.ts
prisma7.config.ts
import "dotenv/config";
import { defineConfig } from "prisma/config"; 
import { defineConfig } from "@prisma/prisma7/config"; 

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: process.env["DATABASE_URL"],
  },
});

The prisma7 CLI discovers prisma7.config.ts automatically, so no --config flag is needed. Renaming frees the prisma.config.ts name for Prisma ORM 8, which only accepts its own config format under that name.

1.4. Point scripts at the prisma7 binary

package.json (excerpt)
{
  "scripts": {
    "prisma:generate": "prisma generate", 
    "db:migrate": "prisma migrate dev"
    "prisma7:generate": "prisma7 generate", 
    "prisma7:migrate": "prisma7 migrate dev"
  }
}

After Prisma ORM 8 is installed, the prisma binary runs the Prisma ORM 8 CLI. Update every script, CI job, and deployment command that must continue using Prisma ORM 7 to call prisma7 instead.

1.5. Check that Prisma ORM 7 still works

bunx prisma7 generate
bunx prisma7 migrate status

Expected result: generate writes the client to generated/prisma as before, and migrate status reports that the database schema is up to date. Start the app and query each route; behavior should be identical to step 1.1.

2. Add Prisma ORM 8

2.1. Install the Prisma ORM 8 packages

bun add --dev prisma@latest
bun add @prisma/orm-postgres

prisma@latest is the Prisma ORM 8 CLI, and its prisma/config subpath provides definePrismaConfig for the Prisma ORM 8 config file. Installing it locally (not only running it through npx) is what makes that import resolve. @prisma/orm-postgres is the PostgreSQL ORM runtime your application code will import.

After this install, npx prisma <command> runs the Prisma ORM 8 CLI and npx prisma7 <command> runs Prisma ORM 7:

bunx prisma --version

Expected result: 8.0.0-rc.14 (or newer). The CLI's version line is separate from @prisma/orm-postgres's.

2.2. Create the Prisma ORM 8 config

prisma.config.ts
import "dotenv/config";
import { definePrismaConfig } from "prisma/config";
import { defineConfig as definePostgresConfig } from "@prisma/orm-postgres/config";

export default definePrismaConfig({
  orm: definePostgresConfig({
    contract: "prisma8/contract.prisma",
    output: "generated/prisma8",
    db: {
      connection: process.env["DATABASE_URL"],
    },
  }),
});

Both configs point at the same DATABASE_URL. Everything else is separate:

Prisma ORM 7Prisma ORM 8
CLIprisma7prisma
Configprisma7.config.tsprisma.config.ts
Schemaprisma/schema.prismaprisma8/contract.prisma
Generated clientgenerated/prismagenerated/prisma8

Because the config carries the connection, the Prisma ORM 8 CLI commands below don't need a --db flag.

2.3. Infer the contract from the live database

Prisma ORM 8 describes your schema as a contract. Generate it from the database Prisma ORM 7 built:

bunx prisma contract infer --output prisma8/contract.prisma

2.4. Edit the inferred contract

The inferred contract needs two edits before it is correct:

  1. Delete the PrismaMigrations model. contract infer picks up Prisma ORM 7's _prisma_migrations ledger table. Prisma ORM 8 must not manage it, and extra tables in the database are fine. Remove the whole model.
  2. Add @@map to every model. Prisma ORM 8 addresses tables by storage name and lowercases unmapped model names, so without @@map("User") it would query public.user. The table Prisma ORM 7 created is "User", so queries fail with relation "public.user" does not exist.

The finished contract:

prisma8/contract.prisma
model User {
  id    Int     @id(map: "User_pkey") @default(autoincrement())
  email String
  name  String?
  posts Post[]

  @@index([email], map: "User_email_key", unique: true)
  @@map("User")
}

model Post {
  id        Int     @id(map: "Post_pkey") @default(autoincrement())
  title     String
  published Boolean @default(false)
  authorId  Int
  author    User    @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Cascade, map: "Post_authorId_fkey")

  @@index([authorId], map: "Post_authorId_idx")
  @@map("Post")
}

2.5. Emit the contract artifacts

bunx prisma contract emit

contract emit writes contract.json and contract.d.ts to generated/prisma8, the runtime and type inputs for the Prisma ORM 8 client. Re-run it after every contract change.

2.6. Include the generated types

The Prisma ORM 8 client imports contract.json with the with { type: "json" } import attribute. This syntax requires TypeScript 5.3 or later and a module setting that supports import attributes. The example project uses "module": "nodenext", which supports them. "esnext" with "moduleResolution": "bundler" also works.

Enable resolveJsonModule so TypeScript types the imported JSON, and include the generated declarations in the program:

tsconfig.json (excerpt)
{
  "compilerOptions": {
    "module": "nodenext",
    "resolveJsonModule": true
  },
  "include": [
    "src/**/*.ts",
    "generated/prisma/**/*.ts",
    "generated/prisma8/**/*.d.ts"
  ]
}

Check: npx tsc --noEmit passes. Prisma ORM 8 is now installed and configured, but no application code uses it yet.

3. Migrate one route

Pick one small route and move only that code. The rest of the application stays on Prisma ORM 7.

3.1. Instantiate both clients

src/db.ts
import "dotenv/config";
import { PrismaPg } from "@prisma/adapter-pg";
import postgres from "@prisma/orm-postgres/runtime"; 
import type { Contract } from "../generated/prisma8/contract.js"; 
import contractJson from "../generated/prisma8/contract.json" with { type: "json" }; 
import { PrismaClient } from "../generated/prisma/client.js";

const connectionString = process.env.DATABASE_URL!;

const adapter = new PrismaPg({ connectionString });

export const prisma = new PrismaClient({ adapter });

export const db = postgres<Contract>({ url: connectionString, contractJson }); 

prisma is the Prisma ORM 7 client and db is the Prisma ORM 8 client, both connected to the same database.

3.2. Rewrite the route

Move the users route to the Prisma ORM 8 ORM client. Queries start from db.orm.<schema>.<Model> (public here) and chain instead of taking one options object:

src/routes/users.ts
import { Hono } from "hono";
import { db } from "../db.js";

export const users = new Hono();

users.get("/", async (c) => {
  const result = await db.orm.public.User.include("posts", (posts) =>
    posts.orderBy((post) => post.id.asc()),
  )
    .orderBy((user) => user.id.asc())
    .all();
  return c.json(result);
});

users.post("/", async (c) => {
  const body = await c.req.json<{ email: string; name?: string }>();
  const user = await db.orm.public.User.create(body);
  return c.json(user, 201);
});

src/routes/posts.ts stays unchanged, on Prisma ORM 7.

3.3. Exercise both code paths

Start the app and hit both routes:

curl localhost:3000/users
curl -X POST localhost:3000/posts -H 'content-type: application/json' \
  -d '{"title":"Written by Prisma 7","authorId":1}'
curl localhost:3000/users

Expected result: the first request runs through Prisma ORM 8. The second writes through Prisma ORM 7. The third, through Prisma ORM 8 again, includes the post Prisma ORM 7 just wrote.

Remaining routes can move over the same way, one at a time, on any schedule. Prisma ORM 7 still owns schema migrations in this phase. If the schema changes, run prisma7 migrate dev, then re-run contract infer and contract emit so the Prisma ORM 8 contract stays current.

4. Transfer migration ownership

So far every schema change has gone through prisma7 migrate dev. In this phase Prisma ORM 8 takes over planning and applying schema changes, and prisma/schema.prisma is frozen.

Treat the switch as a decision, not a routine step. After it, your team and your pipelines must stop using the Prisma ORM 7 migration workflow, even though routes still on the Prisma ORM 7 client keep working. See how migrations work for the full picture.

Prisma ORM 8 tracks schema state with four pieces, and the handoff creates each one exactly once:

  • A contract hash identifies one version of the emitted contract.
  • A migration is an on-disk package recording how to get from one contract hash to another. migrate only replays recorded migrations; it never invents one.
  • The marker is Prisma ORM 8's record, stored in the database, of which contract hash the database currently satisfies.
  • A ref is a named pointer at a contract hash. migration plan uses the db ref as its starting point.

Steps 4.1 to 4.3 create the baseline migration, set the marker, and set the ref.

4.1. Create a baseline migration

bunx prisma migration plan --name baseline

The command writes a migration package under migrations/app/<timestamp>_baseline/ plus a contract snapshot under migrations/snapshots/. It captures the full schema Prisma ORM 7 built, but you will not run it against your database.

4.2. Sign the existing database

Your database already has these tables, so adopt it instead of replaying the baseline:

bunx prisma db sign

db sign verifies the live schema matches the emitted contract and writes Prisma ORM 8's marker at that contract version.

Expected result: Database signed. Then confirm nothing is pending:

bunx prisma migration status

The current and target contract hashes should match, with the baseline migration listed as already satisfied.

4.3. Set the db ref

Point a ref named db at the baseline, using the directory name from step 4.1:

bunx prisma migration ref set db <timestamp>_baseline

Without a ref, the next migration plan has no origin. Because migrations now exist on disk, it refuses with MIGRATION.PLAN_ORIGIN_UNKNOWN rather than plan a full CREATE TABLE package. With the ref set, plans chain from the baseline and contain only your actual changes.

4.4. Retire the Prisma ORM 7 migration scripts

Remove prisma7 migrate from your scripts so nobody runs it by accident. Keep prisma7 generate, because the legacy routes still need their client:

package.json (excerpt)
{
  "scripts": {
    "prisma7:generate": "prisma7 generate",
    "prisma7:migrate": "prisma7 migrate dev", 
    "prisma8:migrate": "prisma db migrate --advance-ref db"
  }
}

4.5. Verify the handoff with a schema change

Verify the migration handoff with a small additive schema change. Add a field to the contract:

prisma8/contract.prisma (excerpt)
model User {
  id    Int     @id(map: "User_pkey") @default(autoincrement())
  email String
  name  String?
  bio   String?
  ...
}

Emit, plan, and apply:

bunx prisma contract emit
bunx prisma migration plan --name add_user_bio
bunx prisma db migrate --advance-ref db
bunx prisma db verify

Expected result: migration plan contains a single operation, Add column "bio" to "User". If it fails with MIGRATION.PLAN_ORIGIN_UNKNOWN instead, the db ref from step 4.3 is missing. db migrate applies the migration, --advance-ref db moves the ref so the next plan chains correctly, and db verify reports that marker and schema match the contract.

Restart the app: the Prisma ORM 8 route returns users with bio, and the Prisma ORM 7 route keeps working untouched, because its client doesn't know about the new column. Additive changes like nullable columns are safe next to legacy Prisma ORM 7 code. Be careful with renames or drops of columns that Prisma ORM 7 routes still read.

5. Remove Prisma ORM 7

Migrate the remaining routes as in phase 3. For posts here, that means swapping prisma.post.findMany(...) for db.orm.public.Post.include("author").all() and prisma.post.create({ data }) for db.orm.public.Post.create(data).

When nothing imports generated/prisma anymore, remove Prisma ORM 7:

bun remove @prisma/prisma7 @prisma/client @prisma/adapter-pg
rm prisma7.config.ts
rm -r prisma generated/prisma

Then delete the prisma7:* scripts from package.json and drop generated/prisma/**/*.ts from the include array in tsconfig.json.

Verify the end state:

bunx tsc --noEmit
bunx prisma db verify

Start the app and run a query against every route. The application now runs entirely on Prisma ORM 8, with schema changes managed by prisma migration plan and prisma db migrate.

Next steps

On this page