Prisma ORM 7 to 8 (PostgreSQL)
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.
The migration runs in five phases. The application works at the end of each one.
- 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.
- 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.
- Migrate one route. One route runs on Prisma ORM 8 while the rest stay on Prisma ORM 7, all against the same database.
- Transfer migration ownership. Prisma ORM 8 takes over planning and applying schema changes.
- 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 database signature and a db ref are in place; the first migration plan then writes the baseline migration itself. 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).
- 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, theprisma-clientgenerator, and a driver adapter - TypeScript 5.9 or newer with
"strict": true. Step 2.6 tells you whether to change themodulesetting intsconfig.json
1. Prepare Prisma ORM 7 for side-by-side operation
Section titled “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.
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:
{
"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"
}
}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])
}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"],
},
});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:
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 devpnpm run devyarn devnpm run devcurl -X POST localhost:3000/users -H 'content-type: application/json' \
-d '{"email":"alice@prisma.io","name":"Alice"}'
curl localhost:3000/usersDo not continue until both requests succeed. That confirms the Prisma ORM 7 application works before you change its configuration.
curl -X POST localhost:3000/users -H 'content-type: application/json' \
-d '{"email":"alice@prisma.io","name":"Alice"}'
curl localhost:3000/usersDo 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
Section titled “1.2. Replace the prisma package with @prisma/prisma7”bun remove prisma
bun add --dev @prisma/prisma7@7.10.0pnpm remove prisma
pnpm add --save-dev @prisma/prisma7@7.10.0yarn remove prisma
yarn add --dev @prisma/prisma7@7.10.0npm uninstall prisma
npm install --save-dev @prisma/prisma7@7.10.0@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.
@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.
mv prisma.config.ts prisma7.config.tsimport "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.
{
"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.
bunx prisma7 generate
bunx prisma7 migrate statuspnpm dlx prisma7 generate
pnpm dlx prisma7 migrate statusyarn dlx prisma7 generate
yarn dlx prisma7 migrate statusnpx prisma7 generate
npx prisma7 migrate statusExpected 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.
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.
bun add --dev prisma@latest
bun add @prisma/orm-postgrespnpm add --save-dev prisma@latest
pnpm add @prisma/orm-postgresyarn add --dev prisma@latest
yarn add @prisma/orm-postgresnpm install --save-dev prisma@latest
npm install @prisma/orm-postgresprisma@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:
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 --versionpnpm prisma --versionyarn prisma --versionnpx prisma --versionExpected result: the command prints 8.0.0-rc.17 or a newer version. That number does not match the version of @prisma/orm-postgres, because the CLI is released separately.
Expected result: the command prints 8.0.0-rc.17 or a newer version. That number does not match the version of @prisma/orm-postgres, because the CLI is released separately.
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 7 | Prisma ORM 8 | |
|---|---|---|
| CLI | prisma7 |
prisma |
| Config | prisma7.config.ts |
prisma.config.ts |
| Schema | prisma/schema.prisma |
prisma8/contract.prisma |
| Generated client | generated/prisma |
generated/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
Section titled “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.prismapnpm prisma contract emityarn prisma contract emitnpx prisma contract emitcontract 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.
contract infer reads every table in the database, so it also writes a PrismaMigrations model for _prisma_migrations, the table where Prisma ORM 7 records which migrations it has applied. Delete that model from the contract, because Prisma ORM 8 must not manage the table. The table itself stays in the database, and an extra table that the contract does not describe is fine.
You do not need to add @@map to the other models: a model without @@map uses its name, exactly as written, as its table name, so User reads and writes the "User" table that Prisma ORM 7 created.
After that edit, the contract looks like this:
// use prisma-8
model User {
id Int @id(map: "User_pkey") @default(autoincrement())
email String
name String?
posts Post[]
@@index([email], map: "User_email_key", unique: true)
}
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")
}Keep // use prisma-8 as the first line of prisma8/contract.prisma. contract emit reads only .prisma files that start with this line, and this is the only file in your contract, so without it contract emit fails with CONTRACT.SOURCE_LOAD_FAILED. A schema.prisma that Prisma ORM 8 reads through prisma7Schema(...), as in the tip at the end of step 2.2, does not need the line.
bunx prisma contract emitpnpm prisma db sign
pnpm prisma migration statusyarn prisma db sign
yarn prisma migration statusnpx prisma db sign
npx prisma migration statusdb sign verifies that the live schema matches the emitted contract, writes Prisma ORM 8's marker at that contract version, stores the contract snapshot under migrations/snapshots/, and points a ref named db at it. migration plan starts from that ref, so plans chain from the schema Prisma ORM 7 built and contain only your own changes.
Expected result: Database signed, and migration status shows the current and target contract hashes match with nothing pending.
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. Set up tsconfig.json for the Prisma ORM 8 client
Section titled “2.6. Set up tsconfig.json for the Prisma ORM 8 client”Your tsconfig.json must allow the JSON import you will write in step 3.1: import contractJson from "../generated/prisma8/contract.json" with { type: "json" }. TypeScript accepts the with { type: "json" } part only when module is esnext, node18, node20, nodenext, or preserve, and only in a file it treats as an ES module. Any other value, such as commonjs, es2022, or node16, rejects it, and leaving module unset fails too. Take the first case below that matches how your app runs:
-
moduleis already one of the five values and your build works with it, and eitherpackage.jsonhas"type": "module"ormoduleisesnextorpreserve. Keep it. SetmoduleResolutiontobundlerifmoduleisesnextorpreserve, tonodenextifmoduleisnodenext, and remove it ifmoduleisnode18ornode20. AmoduleResolutionofnodeornode10cannot resolve the@prisma/orm-*packages, so do not leave that in place. -
You compile with
tsc, run the output withnode, andpackage.jsonhas no"type": "module"(or has"type": "commonjs"). Your app runs as CommonJS, and it can stay that way. Set"module": "nodenext"and"moduleResolution": "nodenext", and do not add"type": "module". Prisma ORM 8 works from CommonJS code, and your relative imports stay as they are, without extensions. TypeScript treatsdb.tsas CommonJS here, so in step 3.1 write the JSON import without thewithpart and copy the other import lines as shown.(CommonJS import contractJson from "../generated/prisma8/contract.json"; -
The app runs through
tsx, or a bundler such asvite,next, oresbuildbuilds it, and you never runtscoutput with plainnode. Set"module": "preserve"and"moduleResolution": "bundler". You do not need file extensions on relative imports, and the import lines in step 3.1 work as shown. If you run the.tsfiles directly withnodeinstead, its type stripping needs the.tsextension on every relative import, so add"allowImportingTsExtensions": trueas well. -
You compile with
tsc, run the output withnode, andpackage.jsonhas"type": "module", even if you usetsxin development. Set"module": "nodenext"and"moduleResolution": "nodenext". Node requires a file extension on every relative import when it runs ES modules, so every import that starts with./or../and points at a.tsor.d.tsfile must end in.js, the extension of the compiled output:import { postsRoutes } from "./routes/posts.js", and a directory import such as./routesbecomes./routes/index.js. Thecontract.jsonimport in step 3.1 keeps its.jsonextension and itswith { type: "json" }part. Until every relative import has its extension,npx tsc --noEmitfails and names each one that still needs it. The example project is set up this way; the TypeScript module reference has the details.
Whichever case applies, add "resolveJsonModule": true so TypeScript can type the JSON import, and add generated/prisma8/**/*.d.ts to include so it sees the emitted contract.d.ts. If your tsconfig.json has no include, TypeScript already picks up every file under the project, so skip that part:
{
"compilerOptions": {
"resolveJsonModule": true
},
"include": [
"src/**/*.ts",
"generated/prisma/**/*.ts",
"generated/prisma8/**/*.d.ts"
]
}Check: npx tsc --noEmit passes. If you followed the last case, it fails until every relative import of a .ts or .d.ts file has its .js extension. Prisma ORM 8 is now installed and configured, but no application code uses it yet. This check does not test your module choice, because nothing imports contract.json until step 3.1. If TypeScript reports error TS2823 there, the module value rejects the with part; come back to this step and pick again. If it reports TS2856, TypeScript compiles db.ts as CommonJS, so remove the with part as the CommonJS case shows.
Pick one small route and move only that code. The rest of the application stays on Prisma ORM 7.
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.
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:
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);
});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 stays unchanged, on Prisma ORM 7.
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/usersExpected 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.
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.
migrateonly 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 planuses thedbref as its starting point.
Step 4.1 creates the marker and the ref with one command. The baseline migration is written by the first migration plan in step 4.3.
Your database already has every table the contract describes, so adopt it rather than replay anything:
bunx prisma db sign
bunx prisma migration statuspnpm prisma contract emit
pnpm prisma migration plan --name add_user_bio
pnpm prisma db migrate --advance-ref db
pnpm prisma db verifyyarn prisma contract emit
yarn prisma migration plan --name add_user_bio
yarn prisma db migrate --advance-ref db
yarn prisma db verifynpx prisma contract emit
npx prisma migration plan --name add_user_bio
npx prisma db migrate --advance-ref db
npx prisma db verifyExpected result: migration plan reports Planned baseline + 1 operation(s) and writes two packages under migrations/app/: a baseline that records the schema you adopted in step 4.1, and add_user_bio with the single operation Add column "bio" to "User". db migrate skips the baseline, because the marker already records that state, and applies add_user_bio. --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.
db sign verifies that the live schema matches the emitted contract, writes Prisma ORM 8's marker at that contract version, stores the contract snapshot under migrations/snapshots/, and points a ref named db at it. migration plan starts from that ref, so plans chain from the schema Prisma ORM 7 built and contain only your own changes.
Expected result: Database signed, and migration status shows the current and target contract hashes match with nothing pending.
4.2. Retire the Prisma ORM 7 migration scripts
Section titled “4.2. 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:
{
"scripts": {
"prisma7:generate": "prisma7 generate",
"prisma7:migrate": "prisma7 migrate dev",
"prisma8:migrate": "prisma db migrate --advance-ref db"
}
}4.3. Verify the handoff with a schema change
Section titled “4.3. Verify the handoff with a schema change”Verify the migration handoff with a small additive schema change. Add a field to the contract:
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 verifypnpm remove @prisma/prisma7 @prisma/client @prisma/adapter-pgyarn remove @prisma/prisma7 @prisma/client @prisma/adapter-pgnpm uninstall @prisma/prisma7 @prisma/client @prisma/adapter-pgrm prisma7.config.ts
rm -r prisma generated/prismaThen delete the prisma7:* scripts from package.json and drop generated/prisma/**/*.ts from the include array in tsconfig.json.
Verify the end state:
Expected result: migration plan reports Planned baseline + 1 operation(s) and writes two packages under migrations/app/: a baseline that records the schema you adopted in step 4.1, and add_user_bio with the single operation Add column "bio" to "User". db migrate skips the baseline, because the marker already records that state, and applies add_user_bio. --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.
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-pgpnpm tsc --noEmit
pnpm prisma db verifyyarn tsc --noEmit
yarn prisma db verifynpx tsc --noEmit
npx prisma db verifyStart 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.
[!NOTE] Prisma ORM 7's
_prisma_migrationstable remains in the database. It is inert (Prisma ORM 8 ignores it) and you can drop it whenever you like.
rm prisma7.config.ts
rm -r prisma generated/prismaThen 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 verifyStart 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.
- How migrations work in Prisma ORM: the day-to-day
contract emit→migration plan→migrateloop for schema changes - Contract authoring: the full PSL syntax for evolving
contract.prisma - Prisma ORM CLI reference: every command used in this guide