# pnpm workspaces (/docs/guides/deployment/pnpm-workspaces)

> For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/llms.txt). A markdown version of any docs page is available by appending `.md` to 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 [#introduction]

This guide shows you how to set up Prisma 8 in its own package inside a [pnpm workspaces](https://pnpm.io/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](https://www.prisma.io/docs/guides/v7/deployment/pnpm-workspaces).

## Prerequisites [#prerequisites]

* [Node.js](https://nodejs.org) 24 or later
* [pnpm](https://pnpm.io/installation) 10 or later (this guide uses pnpm 12)
* A PostgreSQL connection string, or nothing at all: `npx create-db@latest` can create a [Prisma Postgres](https://www.prisma.io/docs/postgres) database for you

## Use with your agent [#use-with-your-agent]

To delegate this guide to your coding agent, copy the prompt below and hand it over:

```text
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.runtime().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 `"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 [#1-create-the-workspace]

Create the monorepo directory and initialize it:

```bash
mkdir my-monorepo
cd my-monorepo
pnpm init
```

pnpm 12 writes a root `package.json` with `"type": "module"` and a pinned `packageManager` field. Next, create `pnpm-workspace.yaml`:

```yaml title="pnpm-workspace.yaml"
packages:
  - "apps/*"
  - "packages/*"

allowBuilds:
  esbuild: true
  msgpackr-extract: true
  workerd: true
```

The `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:

```bash
mkdir -p apps packages/database
```

## 2. Set up the shared database package [#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 [#21-initialize-prisma-8-in-the-package]

```bash
cd packages/database
pnpm init
npx prisma@latest orm init --target postgres
```

Answer the prompts: choose `PSL` for the authoring style and keep the default 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:

```text no-copy
▸ 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.4.0
✔ pnpm add -D @prisma/cli-engine@0.4.0
▸ 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.json
```

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:

```bash
pnpm prisma contract emit
```

`orm init` wrote a starter contract with `User` and `Post` models in `src/prisma/contract.prisma`, and a client in `src/prisma/db.ts`:

```typescript title="packages/database/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 [#22-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:

```bash title="packages/database/.env"
DATABASE_URL="postgres://user:password@localhost:5432/mydb"
```

Apply the contract to the database and sign it:

```bash
pnpm prisma db init
```

```text no-copy
✔ 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 1e8412e162dbbe69f4bb3bf8d07f0280ae67eaab15c34dcf201e67468315428d
✔ Advanced ref "db" → 1e8412e162dbbe69f4bb3bf8d07f0280ae67eaab15c34dcf201e67468315428d
```

If `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 [#23-export-the-client-and-add-a-seed-script]

Create the package entry point that apps will import:

```typescript title="packages/database/src/index.ts"
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:

```typescript title="packages/database/src/seed.ts"
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.runtime().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:

```json title="packages/database/package.json"
{
  "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:

```bash
pnpm seed
```

```text no-copy
$ 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 [#3-set-up-the-nextjs-app]

### 3.1. Scaffold the app into the workspace [#31-scaffold-the-app-into-the-workspace]

```bash
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:

```bash
rm -rf web/.git web/pnpm-workspace.yaml
```

Add the shared package as a dependency of the app:

```json title="apps/web/package.json"
"dependencies": {
  "database": "workspace:*", // [!code ++]
  "next": "16.3.4",
  "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:

```bash
cp ../packages/database/.env web/.env
```

Install from the workspace root so pnpm links `database` into the app:

```bash
cd ..
pnpm install
```

```text no-copy
Scope: all 3 workspace projects
Progress: resolved 8, reused 454, downloaded 0, added 8, done
Done in 5.8s using pnpm v12.3.4
```

### 3.2. Render users from the shared package [#32-render-users-from-the-shared-package]

Replace `apps/web/app/page.tsx` with a server component that queries through the shared client:

```tsx title="apps/web/app/page.tsx"
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 [#33-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`:

```json title="package.json"
"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 [#34-run-the-app]

From the workspace root:

```bash
pnpm dev
```

```text no-copy
$ next dev
▲ Next.js 16.3.4 (Turbopack)
- Local:         http://localhost:3000
- Environments: .env
✓ Ready in 479ms
```

Open [http://localhost:3000](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 [#4-build-for-production]

```bash
pnpm build
```

```text no-copy
$ prisma contract emit
$ next build
▲ Next.js 16.3.4 (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-found
```

The type check runs across the package boundary: `next build` type-checks `packages/database/src/index.ts` along with the app. Then serve the build:

```bash
pnpm start
```

The page renders the same users from the production server.

## 5. (Optional) Browse your data in Prisma Studio [#5-optional-browse-your-data-in-prisma-studio]

[Prisma Studio](https://www.prisma.io/docs/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:

```bash
pnpm dlx prisma@prev studio --url "postgres://user:password@localhost:5432/mydb"
```

```text no-copy
Prisma Studio is running at: http://localhost:51212
```

Open the URL Studio prints. The `user` and `post` tables appear under **Tables**, with the seeded rows ready to edit. See [Studio with Prisma 8](https://www.prisma.io/docs/studio/prisma-next) for the migration history view.

## Common gotchas [#common-gotchas]

> [!WARNING]
> `pnpm dlx prisma@latest` stops at an interactive **Choose which packages to build** prompt, because `dlx` runs outside the workspace and ignores its `allowBuilds`. Either answer the prompt, or run `npx prisma@latest` from a package directory as this guide does. At the workspace root, `npx` itself fails with `EBADDEVENGINES`: pnpm 12's `pnpm init` writes a `devEngines.packageManager` field that npm enforces. Use the package's own CLI (`pnpm prisma ...`) or `pnpm dlx` there.

* If `pnpm prisma contract emit` reports `CLI.CONFIG_UNREADABLE` with `Cannot find module '@prisma/cli-engine'` or `No "exports" main defined`, the `@prisma/cli-engine` link in `packages/database/node_modules` is dangling. `pnpm install --force` does not repair it; `pnpm add -D prisma@latest @prisma/cli-engine@latest` does.
* Every Prisma command reads `DATABASE_URL` from `packages/database/.env` through `prisma.config.ts`, and the app reads `apps/web/.env`. Keep the two files in sync, or export the variable in your shell and drop both files.
* Do not call `db.runtime().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, like `seed.ts`.
* After you change `src/prisma/contract.prisma`, run `pnpm --filter database contract:emit` so the app sees the new types, then `pnpm db:update` to apply the change. The root `build` script emits for you before every build.

## Prompt your coding agent [#prompt-your-coding-agent]

Run [`pnpm prisma init`](https://www.prisma.io/docs/cli/init) once inside `packages/database` to install the [Prisma 8 skills](https://www.prisma.io/docs/ai/tools/skills#available-skills-for-prisma-8) 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 `Post` list under each user on the home page with `.include('posts')`."
* "Add a `packages/database` script that creates a post for a given user email."
* "Add a `role` enum to the `User` model in `contract.prisma`, emit the contract, and update the database."

## Next steps [#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](https://www.prisma.io/docs/guides/deployment/turborepo) guide.
* [Learn the fundamentals](https://www.prisma.io/docs/orm/fundamentals/reading-data): filtering, sorting, pagination, and writes.
* [Read the Prisma 8 overview](https://www.prisma.io/docs/orm) for the concepts behind contracts and typed queries.
* Use [migration plan](https://www.prisma.io/docs/cli/migration-plan) and [db migrate](https://www.prisma.io/docs/cli/db-migrate) when you want checked-in migrations instead of `db update`.

## Related pages

- [`Bun workspaces`](https://www.prisma.io/docs/guides/deployment/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`](https://www.prisma.io/docs/guides/deployment/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`](https://www.prisma.io/docs/guides/deployment/docker): Build an Express app on Prisma ORM, run PostgreSQL from Docker Compose, then run the app and the database together in containers.
- [`Turborepo`](https://www.prisma.io/docs/guides/deployment/turborepo): Share one Prisma 8 database package across the apps in a Turborepo monorepo, with contract emit and migrations wired into turbo tasks.