pnpm workspaces (/docs/guides/deployment/pnpm-workspaces)
For the complete Prisma documentation index, see llms.txt. A markdown version of any docs page is available by appending
.mdto its URL.
Set up Prisma 8 in a shared database package inside a pnpm workspaces monorepo and query it from a Next.js app.
Location: Guides > Deployment > pnpm workspaces
Introduction
Section titled “Introduction”This guide shows you how to set up Prisma 8 in its own package inside a pnpm workspaces monorepo. The database package owns the contract, the emitted types, and the client. A Next.js app in the same workspace imports that client and renders users from the database.
Every command and output below was run end to end with pnpm 12 against a PostgreSQL database.
[!NOTE] Using Prisma 7?
Prisma 8 is the current release of Prisma ORM. Prisma 7 remains fully supported; the Prisma 7 version of this guide is at /guides/v7/deployment/pnpm-workspaces.
Prerequisites
Section titled “Prerequisites”- Node.js 24 or later
- pnpm 10.26 or later, but not 12.0 to 12.4.0, which have a defect that breaks step 2.1 (this guide uses pnpm 12.7.0)
- A PostgreSQL connection string, or nothing at all:
npx create-db@latestcan create a Prisma Postgres database for you
Use with your agent
Section titled “Use with your agent”To delegate this guide to your coding agent, copy the prompt below and hand it over:
Set up a pnpm workspaces monorepo with a shared Prisma 8 database package and a Next.js app that renders users from it.
1. Create `my-monorepo` with `pnpm init`, a `pnpm-workspace.yaml` listing `apps/*` and `packages/*` with `allowBuilds` for `esbuild`, `msgpackr-extract`, and `workerd`, and the directories `apps` and `packages/database`.
2. In `packages/database`, run `pnpm init`, then `npx prisma@latest orm init --yes --target postgres --authoring psl`. Then run `pnpm prisma init` in the same directory so the Prisma agent skills are installed, and use them.
3. Write `packages/database/.env` with `DATABASE_URL` (use the connection string I give you, or create a Prisma Postgres database with `npx create-db@latest` and show me the claim URL it prints). Run `pnpm prisma db init` in `packages/database`.
4. Add `src/index.ts` exporting `db` from `./prisma/db`, set `"exports": { ".": "./src/index.ts" }` in the package's package.json, add a `src/seed.ts` that upserts two users with `db.orm.public.User.upsert(...)` and closes with `await db.close()`, and run it with `node src/seed.ts`.
5. In `apps`, run `pnpm create next-app@latest web --yes --skip-install`, delete `apps/web/.git` and `apps/web/pnpm-workspace.yaml`, add `sharp: false` and `unrs-resolver: false` to `allowBuilds` in the root `pnpm-workspace.yaml`, add `"database": "workspace:*"` to `apps/web/package.json`, copy `packages/database/.env` to `apps/web/.env`, and run `pnpm install` from the workspace root.
6. Replace `apps/web/app/page.tsx` with a server component that imports `{ db } from "database"`, exports `dynamic = "force-dynamic"`, queries `db.orm.public.User.select("id", "email", "name").all()`, and renders the list.
7. Add root scripts `dev`, `build`, `start`, `db:init`, `db:update`, and `seed` that filter to the right package, start `pnpm dev` in the background, verify http://localhost:3000 renders the seeded users, then stop it. Finally run `pnpm build` and confirm it completes.1. Create the workspace
Section titled “1. Create the workspace”Create the monorepo directory and initialize it:
mkdir my-monorepo
cd my-monorepo
pnpm initpnpm 12 writes a root package.json with "type": "module" and a pinned packageManager field. Next, create pnpm-workspace.yaml:
packages:
- "apps/*"
- "packages/*"
allowBuilds:
esbuild: true
msgpackr-extract: true
workerd: trueThe allowBuilds block matters. pnpm does not run dependency install scripts unless you approve them. Since pnpm 11 an unapproved script fails the install (strictDepBuilds is on by default); pnpm 10 only warns and skips the script, which leaves the package half-installed, unless you set strictDepBuilds: true. The Prisma ORM 8 CLI's toolchain pulls in three packages with install scripts, so approve them up front. The allowBuilds key exists since pnpm 10.26, so use that version or later; without the key, the first pnpm add that orm init runs fails with ERR_PNPM_IGNORED_BUILDS on pnpm 11 (on pnpm 10 with the default settings it installs the three packages without running their scripts).
Create the directories for apps and shared packages:
mkdir -p apps packages/database2. Set up the shared database package
Section titled “2. Set up the shared database package”The database package holds the contract (your schema), the emitted contract.json and contract.d.ts, and the db client every app imports. Prisma 8 has no prisma generate step and no generated client directory: contract emit writes the two artifacts next to the contract, and the runtime reads them.
2.1. Initialize Prisma 8 in the package
Section titled “2.1. Initialize Prisma 8 in the package”cd packages/database
pnpm init
npx prisma@latest orm init --target postgresAnswer the prompts: choose PSL for the authoring style and keep the other defaults, including the schema path src/prisma/contract.prisma. orm init detects pnpm from the workspace, adds the dependencies to this package, and writes the Prisma 8 files:
▸ pnpm add @prisma/orm-postgres dotenv
✔ pnpm add @prisma/orm-postgres dotenv
▸ pnpm add -D prisma@latest @types/node
✔ pnpm add -D prisma@latest @types/node
▸ pnpm add -D @prisma/cli-engine@0.6.1
✔ pnpm add -D @prisma/cli-engine@0.6.1
▸ Emit the contract
✔ Emit the contract
│ target: postgres
│ authoring: psl
│ schema: src/prisma/contract.prisma
written
├─ src/prisma/contract.prisma
├─ prisma.config.ts
├─ src/prisma/db.ts
├─ prisma-8.md
├─ .env.example
├─ tsconfig.json
├─ .gitignore
├─ .gitattributes
└─ package.jsonOn pnpm 12.0 to 12.4.0, orm init writes all the files and then fails at Emit the contract with CLI.INIT_EMIT_FAILED. If you hit it, apply the pnpm dedupe fix from Common gotchas and continue.
The @prisma/cli-engine version comes from the prisma package the previous step installed, so the two always match. orm init emits the contract itself, so src/prisma/contract.json and src/prisma/contract.d.ts exist as soon as it finishes. After every schema edit, re-emit with the package's own CLI:
pnpm prisma contract emitorm init wrote a starter contract with User and Post models in src/prisma/contract.prisma, and a client in src/prisma/db.ts:
import 'dotenv/config';
import postgres from '@prisma/orm-postgres/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL']!,
});There is no driver adapter and no engine to configure. The with { type: 'json' } import attribute is required by Node's ESM loader; pnpm's pnpm init already set "type": "module" on the package, which is what the generated files expect. If your package declares "type": "commonjs", orm init leaves it alone and prints a warning; change it to "module".
2.2. Connect the database
Section titled “2.2. Connect the database”prisma.config.ts loads .env through dotenv/config and reads DATABASE_URL. Create .env in the package with your connection string, or the one npx create-db@latest prints:
DATABASE_URL="postgres://user:password@localhost:5432/mydb"Apply the contract to the database and sign it:
pnpm prisma db init✔ Initialising database across spaces
│ contract: src/prisma/contract.json
│ database: postgres://****@localhost:5432/mydb
✔ Applied 5 operation(s) across 1 contract space
App space
├─ Create table "Post"
├─ Create table "User"
├─ Add unique constraint on "User" (email)
├─ Create index "Post_authorId_idx_e47547ed" on "Post"
├─ Add foreign key "Post_authorId_fkey" on "Post"
└─ marker 91e7f9f035806fa2789a4d726ef7724cad434fd6b00014d47ebf12d6e6bb784e
✔ Advanced ref "db" → 91e7f9f035806fa2789a4d726ef7724cad434fd6b00014d47ebf12d6e6bb784eIf db init stops with Connection terminated unexpectedly, a database you just created is still starting; wait a few seconds and run it again. The command is safe to repeat and reports Database already matches contract when there is nothing left to do.
2.3. Export the client and add a seed script
Section titled “2.3. Export the client and add a seed script”Create the package entry point that apps will import:
export { db } from "./prisma/db";Add a seed script so the app has rows to render. Node.js 24 runs TypeScript directly, but it needs the .ts extension on relative imports, so this file names it:
import { db } from "./prisma/db.ts";
const users = [
{ email: "alice@prisma.io", name: "Alice" },
{ email: "bob@prisma.io", name: "Bob" },
];
for (const user of users) {
await db.orm.public.User.upsert({
create: user,
update: {},
conflictOn: { email: user.email },
});
}
console.log(await db.orm.public.User.select("id", "email", "name").all());
await db.close();Point the package at the entry point and add scripts for the database steps. Replace the main field pnpm init wrote with an exports map, and drop the placeholder test script:
{
"name": "database",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"contract:emit": "prisma contract emit",
"db:init": "prisma db init",
"db:update": "prisma db update",
"seed": "node src/seed.ts"
}
}Keep the dependencies and devDependencies that orm init added. Run the seed:
pnpm seed$ node src/seed.ts
[
{ id: 1, email: 'alice@prisma.io', name: 'Alice' },
{ id: 2, email: 'bob@prisma.io', name: 'Bob' }
]The script resolves @prisma/orm-postgres through pnpm's isolated node_modules without any hoisting configuration, because the package declares it as a direct dependency. That is the rule to keep in mind for the rest of the workspace: the database package depends on @prisma/orm-postgres, and apps depend on database.
3. Set up the Next.js app
Section titled “3. Set up the Next.js app”3.1. Scaffold the app into the workspace
Section titled “3.1. Scaffold the app into the workspace”cd ../../apps
pnpm create next-app@latest web --yes --skip-install--yes accepts the defaults (App Router, TypeScript, Tailwind CSS, no src/ directory). --skip-install keeps create-next-app from installing into a nested node_modules; the workspace root installs for every package. The scaffold still writes two files that belong to the root, so remove them:
rm -rf web/.git web/pnpm-workspace.yamlThe pnpm-workspace.yaml you removed told pnpm not to run the install scripts of two of the app's dependencies. Move that decision to the root file, or the pnpm install below fails with ERR_PNPM_IGNORED_BUILDS:
packages: - "apps/*" - "packages/*"allowBuilds: esbuild: true msgpackr-extract: true sharp: false unrs-resolver: false workerd: trueAdd the shared package as a dependency of the app:
"dependencies": { "database": "workspace:*", "next": "16.3.6", "react": "19.2.8", "react-dom": "19.2.8"}Next.js reads .env from the app directory, and db.ts reads it from the working directory of the dev server, which is the same place. Copy the file from the database package:
cp ../packages/database/.env web/.envInstall from the workspace root so pnpm links database into the app:
cd ..
pnpm installScope: all 3 workspace projects
Progress: resolved 700, reused 700, downloaded 0, added 700, done
Done in 1.9s using pnpm v12.7.03.2. Render users from the shared package
Section titled “3.2. Render users from the shared package”Replace apps/web/app/page.tsx with a server component that queries through the shared client:
import { db } from "database";
export const dynamic = "force-dynamic";
export default async function Home() {
const users = await db.orm.public.User.select("id", "email", "name").all();
return (
<main className="p-8">
<h1 className="text-2xl font-semibold">Users</h1>
{users.length === 0 ? (
<p>No users in the database yet.</p>
) : (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name ?? "Anonymous"} ({user.email})
</li>
))}
</ul>
)}
</main>
);
}Model access is namespace-qualified on PostgreSQL: db.orm.public.User. force-dynamic makes Next.js query on each request instead of at build time, so pnpm build does not need a reachable database. Next.js compiles the package's TypeScript source through the exports map; no transpilePackages entry is needed.
3.3. Add root scripts
Section titled “3.3. Add root scripts”Add scripts to the root package.json that run each step in the right package. build re-emits the contract before the app builds, so the types the app compiles against always match contract.prisma:
"scripts": {
"dev": "pnpm --filter web dev",
"build": "pnpm --filter database contract:emit && pnpm --filter web build",
"start": "pnpm --filter web start",
"db:init": "pnpm --filter database db:init",
"db:update": "pnpm --filter database db:update",
"seed": "pnpm --filter database seed"
}3.4. Run the app
Section titled “3.4. Run the app”From the workspace root:
pnpm dev$ next dev
▲ Next.js 16.3.6 (Turbopack)
- Local: http://localhost:3000
- Environments: .env
✓ Ready in 479msOpen http://localhost:3000 (set PORT to change it). The page lists Alice and Bob, rendered by a server component calling Prisma 8 through the database package.
4. Build for production
Section titled “4. Build for production”pnpm build$ prisma contract emit
$ next build
▲ Next.js 16.3.6 (Turbopack)
✓ Compiled successfully in 1026ms
Running TypeScript ...
Finished TypeScript in 2.2s ...
✓ Generating static pages using 5 workers (3/3) in 519ms
Route (app)
┌ ƒ /
└ ○ /_not-foundThe type check runs across the package boundary: next build type-checks packages/database/src/index.ts along with the app. Then serve the build:
pnpm startThe page renders the same users from the production server.
5. (Optional) Browse your data in Prisma Studio
Section titled “5. (Optional) Browse your data in Prisma Studio”Prisma Studio ships with the Prisma 7 CLI and connects to a Prisma 8 database through --url. Run it from the workspace root, which has no prisma.config.ts for the Prisma 7 CLI to trip over:
pnpm dlx prisma@prev studio --url "postgres://user:password@localhost:5432/mydb"Prisma Studio is running at: http://localhost:51212Open the URL Studio prints. The User and Post tables appear under Tables, with the seeded rows ready to edit. See Studio with Prisma 8 for the migration history view.
Common gotchas
Section titled “Common gotchas”[!WARNING]
pnpm dlx prisma@lateststops at an interactive Choose which packages to build prompt, becausedlxruns outside the workspace and ignores itsallowBuilds. Either answer the prompt, or runnpx prisma@latestfrom a package directory as this guide does. At the workspace root,npxitself fails withEBADDEVENGINES: pnpm 12'spnpm initwrites adevEngines.packageManagerfield that npm enforces. Use the package's own CLI (pnpm prisma ...) orpnpm dlxthere.
- If
orm initends withCLI.INIT_EMIT_FAILED, orpnpm prisma contract emitreportsCLI.CONFIG_UNREADABLE, and the message saysCannot find module '@prisma/cli-engine'orNo "exports" main defined, the@prisma/cli-enginelink inpackages/database/node_modulespoints at a directory that does not exist. This is a defect in pnpm 12.0 to 12.4.0, and pnpm 12.4.1 or later fixes it. In a project that already hit it, runpnpm dedupeto rebuild the link, thenpnpm prisma contract emit. You do not need to runorm initagain, because it wrote all its files before it tried to emit.pnpm install,pnpm install --force, and adding the packages again do not repair the link. - Every Prisma command reads
DATABASE_URLfrompackages/database/.envthroughprisma.config.ts, and the app readsapps/web/.env. Keep the two files in sync, or export the variable in your shell and drop both files. - Do not call
db.close()in a page or route handler. The client is a module-level singleton whose connection pool is shared across requests; close it only in scripts that exit, likeseed.ts. - After you change
src/prisma/contract.prisma, runpnpm --filter database contract:emitso the app sees the new types, thenpnpm db:updateto apply the change. The rootbuildscript emits for you before every build.
Prompt your coding agent
Section titled “Prompt your coding agent”Run pnpm prisma init once inside packages/database to install the Prisma 8 skills for your coding agent. It adds a postinstall script that keeps the skills matching your installed packages, and it installs them into .claude/skills, .cursor/skills, .agents/skills, and .devin/skills under the package. Prompts that map to this guide:
- "Using the prisma-8 skill, add a
Postlist under each user on the home page with.include('posts')." - "Add a
packages/databasescript that creates a post for a given user email." - "Add a
roleenum to theUsermodel incontract.prisma, emit the contract, and update the database."
Next steps
Section titled “Next steps”You now have a pnpm workspace where one package owns the Prisma 8 contract and client, and a Next.js app that renders through it.
- To add task orchestration and caching on top of this setup, see the Turborepo guide.
- Learn the fundamentals: filtering, sorting, pagination, and writes.
- Read the Prisma 8 overview for the concepts behind contracts and typed queries.
- Use migration plan and db migrate when you want checked-in migrations instead of
db update.
Related pages
Section titled “Related pages”Bun workspaces: Set up Prisma ORM in a Bun workspaces monorepo through a shared database package, seed it with Bun, and query it from a Next.js app in the same workspace.Cloudflare Workers: Add Prisma ORM to a Cloudflare Worker, query PostgreSQL from the fetch handler with the nodejs_compat flag, and deploy it with Wrangler.Docker: Build an Express app on Prisma ORM, run PostgreSQL from Docker Compose, then run the app and the database together in containers.Turborepo: Share one Prisma 8 database package across the apps in a Turborepo monorepo, with contract emit and migrations wired into turbo tasks.