# Bun workspaces (/docs/guides/deployment/bun-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 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.

Location: Guides > Deployment > Bun workspaces

## Introduction [#introduction]

This guide shows you how to use Prisma ORM in a [Bun workspaces](https://bun.sh/docs/install/workspaces) monorepo. You create a shared `database` package that owns the schema, the database client, and a seed script, then import that client from a Next.js app in the same workspace. Every workspace member shares one lockfile and one `bun install`, so the database package is wired into the app like any other dependency.

Every command, file, and output below was run end to end against a live PostgreSQL database.

> [!NOTE]
> Using Prisma ORM 7?
> 
> Prisma ORM 8 is the current release. Prisma ORM 7 remains fully supported; the Prisma ORM 7 version of this guide is at [/guides/v7/deployment/bun-workspaces](https://www.prisma.io/docs/guides/v7/deployment/bun-workspaces).

## Prerequisites [#prerequisites]

* [Bun](https://bun.sh/docs/installation) 1.1 or later (`bun --version`)
* [Node.js](https://nodejs.org) 24 or later: Bun runs the scripts, and Next.js runs on Node.js
* A PostgreSQL connection string, or nothing at all: `bunx 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
Create a Bun workspaces monorepo with a shared Prisma ORM database package and a Next.js app that queries it.

1. Create `my-monorepo` with a root `package.json` that sets `"workspaces": ["apps/*", "packages/*"]`, then create `apps/` and `packages/database/`. Give `packages/database` a `package.json` with `"name": "database"`, `"private": true`, `"type": "module"` and `"main": "index.ts"`.
2. In `packages/database`, run `bunx prisma@latest orm init --yes --target postgres --authoring psl`. Then run `bunx prisma@latest init` there so the Prisma agent skills are installed and stay current, and use them. Get a database connection string: use the one I give you, or create a Prisma Postgres database with `bunx create-db@latest` and show me the claim URL it prints. Write it as `DATABASE_URL` into `packages/database/.env`.
3. Add package scripts to `packages/database`: `db:init` (`prisma db init`), `db:update` (`prisma db update`), `db:seed` (`bun src/prisma/seed.ts`). Run `bun run db:init` there.
4. Create `packages/database/index.ts` that re-exports `db` from `./src/prisma/db`, and `packages/database/src/prisma/seed.ts` that upserts three users with `db.orm.public.User.upsert({ create, update: {}, conflictOn: { email } })` and ends with `await db.runtime().close()`, following https://www.prisma.io/docs/guides/deployment/bun-workspaces.md. Run `bun run db:seed`.
5. Add root scripts `dev`, `build`, `start`, `db:init`, `db:update` and `seed` that use `bun run --filter <package> <script>`.
6. In `apps/`, run `bun create next-app@latest web --yes`, delete `apps/web/.git`, add `"database": "workspace:*"` to its dependencies, copy `packages/database/.env` to `apps/web/.env`, and run `bun install` from the root.
7. Replace `apps/web/app/page.tsx` with a server component that exports `dynamic = "force-dynamic"` and lists users from `db.orm.public.User.select("id", "name", "email").all()`.
8. Start `bun run dev` from the root in the background, wait until it reports ready, verify `curl http://localhost:3000` returns the seeded users, then stop the dev server.

Use the installed Prisma ORM skills.
```

## 1. Set up the workspace [#1-set-up-the-workspace]

Create a directory for the monorepo and a root `package.json` that declares where the apps and shared packages live:

```bash
mkdir my-monorepo
cd my-monorepo
```

```json title="package.json"
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"]
}
```

Create the directories for the app and the shared database package:

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

## 2. Set up the database package [#2-set-up-the-database-package]

The `database` package owns the schema, the emitted contract, the Prisma ORM client, and the seed script. Every app in the workspace imports the client from it.

### 2.1. Initialize Prisma ORM [#21-initialize-prisma-orm]

Give the package a `package.json`. `"main": "index.ts"` lets other workspace members import the TypeScript source directly, so there is no build step for the package:

```json title="packages/database/package.json"
{
  "name": "database",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "main": "index.ts"
}
```

Then add Prisma ORM to the package:

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

Choose `PSL` as the authoring style and keep the default schema path. The command detects Bun as the package manager, installs `@prisma/orm-postgres` and `dotenv` plus the dev dependencies `prisma`, `@types/node`, and `@prisma/cli-engine`, and writes:

* `src/prisma/contract.prisma`: the schema, with starter `User` and `Post` models
* `src/prisma/db.ts`: the client the rest of the workspace imports
* `prisma.config.ts`: loads `.env` and points the CLI at the contract and `DATABASE_URL`
* `.env.example`, `tsconfig.json`, `.gitignore`, and `prisma-8.md`
* a `contract:emit` script in `package.json`

It also runs the first `contract emit`, which compiles the schema into `src/prisma/contract.json` and `src/prisma/contract.d.ts`. Those two files are what your queries are type-checked against, so there is no `prisma generate` step and no generated client folder to export from the package.

Bun installs the workspace's dependencies into the root `node_modules` and writes a single `bun.lock` at the root, even though you ran the command inside `packages/database`.

The generated client reads `DATABASE_URL` from the environment and loads `.env` first:

```ts 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 runtime talks to PostgreSQL directly.

### 2.2. Set the connection string and add scripts [#22-set-the-connection-string-and-add-scripts]

Create `.env` in the database package. Use your own PostgreSQL connection string, or create a Prisma Postgres database with `bunx create-db@latest`; it prints a connection string and a claim URL you can open to keep the database.

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

Add the database scripts next to the `contract:emit` script that `orm init` created. The seed script runs with `bun`, which executes TypeScript directly:

```json title="packages/database/package.json"
{
  "scripts": {
    "contract:emit": "prisma contract emit",
    "db:init": "prisma db init", // [!code ++]
    "db:update": "prisma db update", // [!code ++]
    "db:verify": "prisma db verify", // [!code ++]
    "db:seed": "bun src/prisma/seed.ts" // [!code ++]
  }
}
```

### 2.3. Initialize the database [#23-initialize-the-database]

Apply the schema to the database and sign it:

```bash
bun run db:init
```

```text no-copy
"summary": "Applied 5 operation(s) across 1 space(s), database signed"
```

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 `Applied 0 operation(s)` when there is nothing left to do.

### 2.4. Export the client [#24-export-the-client]

Create `index.ts` at the package root. It re-exports the client and the `Contract` type so apps import from `database` and never reach into the package's internals:

```ts title="packages/database/index.ts"
export { db } from "./src/prisma/db";
export type { Contract } from "./src/prisma/contract.d";
```

### 2.5. Seed the database [#25-seed-the-database]

Create the seed script. `upsert` with `conflictOn` makes it safe to run more than once, and `await db.runtime().close()` at the end lets the process exit instead of waiting on the connection pool:

```ts title="packages/database/src/prisma/seed.ts"
import { db } from "./db";

const users = [
  { email: "alice@example.com", name: "Alice" },
  { email: "bob@example.com", name: "Bob" },
  { email: "charlie@example.com", name: "Charlie" },
];

for (const user of users) {
  await db.orm.public.User.upsert({
    create: user,
    update: {},
    conflictOn: { email: user.email },
  });
}

console.log(`Seeded ${users.length} users.`);
await db.runtime().close();
```

Model access is namespace-qualified on PostgreSQL: `db.orm.public.User`, where `public` is the default schema.

Run it:

```bash
bun run db:seed
```

```text no-copy
$ bun src/prisma/seed.ts
Seeded 3 users.
```

The shared database package is now complete.

## 3. Add root scripts [#3-add-root-scripts]

Go back to the workspace root and add scripts that fan out to the packages with `bun run --filter`:

```bash
cd ../..
```

```json title="package.json"
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "scripts": { // [!code ++]
    "dev": "bun run --filter web dev", // [!code ++]
    "build": "bun run --filter database contract:emit && bun run --filter web build", // [!code ++]
    "start": "bun run --filter web start", // [!code ++]
    "db:init": "bun run --filter database db:init", // [!code ++]
    "db:update": "bun run --filter database db:update", // [!code ++]
    "seed": "bun run --filter database db:seed" // [!code ++]
  } // [!code ++]
}
```

`build` emits the contract before building the app, so a schema edit is never stale in a production build. There is no generate step to run before `dev`: the emitted `contract.json` and `contract.d.ts` are plain files that the app imports.

Check that the seed works from the root:

```bash
bun run seed
```

```text no-copy
$ bun run --filter database db:seed
database db:seed: Seeded 3 users.
database db:seed: Exited with code 0
```

## 4. Set up the Next.js app [#4-set-up-the-nextjs-app]

### 4.1. Create the app [#41-create-the-app]

Create a Next.js app named `web` inside `apps/`:

```bash
cd apps
bun create next-app@latest web --yes
```

```text no-copy
Initializing project with template: app-tw

Installing dependencies:
- next
- react
- react-dom
...
Success! Created web at /path/to/my-monorepo/apps/web
```

`--yes` accepts the defaults (App Router, TypeScript, Tailwind, no `src/` directory) and uses Bun as the package manager because you ran it with `bun create`. Bun recognizes that `apps/web` is inside a workspace and records its dependencies in the root `bun.lock`.

The scaffold also initializes a Git repository inside `apps/web`. Remove it, because the monorepo root should be the only repository:

```bash
cd web
rm -rf .git
```

### 4.2. Add the database package [#42-add-the-database-package]

Add the shared package as a workspace dependency:

```json title="apps/web/package.json"
{
  "dependencies": {
    "database": "workspace:*", // [!code ++]
    "next": "16.3.4",
    "react": "19.2.8",
    "react-dom": "19.2.8"
  }
}
```

The client in the database package reads `DATABASE_URL` from `.env` in the current working directory, and Next.js loads `.env` from the app directory. Copy the file so both find it:

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

The Next.js `.gitignore` already excludes `.env*`, so the copy stays out of Git.

Then link the package by installing from the root:

```bash
cd ../..
bun install
```

```text no-copy
bun install v1.3.3 (274e01c7)
Saved lockfile

Checked 478 installs across 616 packages (no changes) [440.00ms]
```

`apps/web/node_modules/database` is now a symlink to `packages/database`.

### 4.3. Query the database from a server component [#43-query-the-database-from-a-server-component]

Replace `apps/web/app/page.tsx` with a server component that lists the users. `dynamic = "force-dynamic"` makes the page query the database on every request instead of once at build time:

```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", "name", "email").all();

  return (
    <main>
      <h1>Users</h1>
      {users.length === 0 && <p>No users have been added to the database yet.</p>}
      <ul>
        {users.map((user) => (
          <li key={user.id}>{`${user.name} (${user.email})`}</li>
        ))}
      </ul>
    </main>
  );
}
```

`users` is typed from the contract, so `user.name` and `user.email` autocomplete in the app even though the schema lives in another package. Turbopack compiles the package's TypeScript source through the workspace symlink; no `transpilePackages` setting is needed.

### 4.4. Run the app [#44-run-the-app]

Start the dev server from the workspace root:

```bash
bun run dev
```

```text no-copy
$ bun run --filter web dev
web dev: ▲ Next.js 16.3.4 (Turbopack)
web dev: - Local:         http://localhost:3000
web dev: - Network:       http://192.168.1.16:3000
web dev: - Environments: .env
web dev: ✓ Ready in 4.0s
```

Open [http://localhost:3000](http://localhost:3000), or check it from another terminal:

```bash
curl -s http://localhost:3000 | grep -o '<h1>.*</ul>'
```

```html no-copy
<h1>Users</h1><ul><li>Alice (alice@example.com)</li><li>Bob (bob@example.com)</li><li>Charlie (charlie@example.com)</li></ul>
```

The production build works the same way. `bun run build` emits the contract and then runs `next build`; `bun run start` serves the result:

```bash
bun run build
```

```text no-copy
$ bun run --filter database contract:emit && bun run --filter web build
database contract:emit: Exited with code 0
web build: ▲ Next.js 16.3.4 (Turbopack)
web build:   Creating an optimized production build ...
web build: ✓ Compiled successfully in 26.2s
web build:   Running TypeScript ...
web build:   Finished TypeScript in 15.8s ...
web build:
web build: Route (app)
web build: ┌ ƒ /
web build: └ ○ /_not-found
web build:
web build: ƒ  (Dynamic)  server-rendered on demand
web build: Exited with code 0
```

## 5. Change the schema [#5-change-the-schema]

Schema changes happen in one place. Edit `packages/database/src/prisma/contract.prisma`, for example to add a field to `User`:

```prisma title="packages/database/src/prisma/contract.prisma"
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  username  String?
  name      String?
  bio       String? // [!code ++]
  posts     Post[]
  createdAt TimestamptzString @default(now())
  updatedAt temporal.updatedAtString()
}
```

Then, from the root, emit the contract and update the database:

```bash
bun run --filter database contract:emit
bun run db:update
```

```text no-copy
"summary": "Applied 1 operation(s) across 1 space(s), signature updated"
```

The Next.js app picks up the new field on its next request because it imports the package's emitted `contract.d.ts` directly. For a checked-in migration instead of a direct update, use [`migration plan`](https://www.prisma.io/docs/cli/migration-plan) and [`db migrate`](https://www.prisma.io/docs/cli/db-migrate).

## Common gotchas [#common-gotchas]

> [!WARNING]
> If the app fails with `CONTRACT.MARKER_READ_FAILED` and `Postgres driver not connected`, `DATABASE_URL` is not set in the app's environment. The client is constructed in `packages/database`, but the environment it reads comes from the process that imports it. Make sure `apps/web/.env` exists, or export `DATABASE_URL` in the shell that runs `bun run dev`.

> [!WARNING]
> `db update` refuses to apply an operation that destroys data, such as dropping a column, without consent. Interactively it asks you to type the database name; in a script or CI job, pass `--confirm <database name>`.

> [!NOTE]
> Without `export const dynamic = "force-dynamic"`, Next.js prerenders the page at build time and the users list is frozen at whatever the database held during `bun run build`.

Do not call `db.runtime().close()` in a server component or route handler. The client in `packages/database` is a module-level singleton whose connection pool is shared across requests; close it only in short-lived scripts such as the seed.

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

Run [`bunx prisma@latest init`](https://www.prisma.io/docs/cli/init) once inside `packages/database` to install the [Prisma ORM skills](https://www.prisma.io/docs/ai/tools/skills#available-skills-for-prisma-8) for your coding agent and keep them matching your installed packages. It adds a `postinstall` script that re-syncs the skills after every `bun install`. Prompts that map to this guide:

* "Using the prisma-8 skill, add a `packages/database/src/prisma/reset.ts` script that deletes all posts and users."
* "Add a `/users/[id]` page in `apps/web` that loads one user with their posts from the shared `database` package."
* "Add an `apps/api` Bun server to the workspace that exposes `GET /users` from the shared `database` package."

## Next steps [#next-steps]

You now have a Bun workspaces monorepo with a shared Prisma ORM database package integrated into a Next.js app.

* To add task orchestration and caching on top of this setup, see the [Turborepo](https://www.prisma.io/docs/guides/deployment/turborepo) guide.
* To serve the same package from a plain Bun server instead of Next.js, see the [Bun](https://www.prisma.io/docs/guides/runtimes/bun) guide.
* [Learn the fundamentals](https://www.prisma.io/docs/orm/fundamentals/reading-data): filtering, sorting, pagination, and writes.
* [Read the Prisma ORM overview](https://www.prisma.io/docs/orm) for the concepts behind contracts and typed queries.

## Related pages

- [`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.
- [`pnpm workspaces`](https://www.prisma.io/docs/guides/deployment/pnpm-workspaces): Set up Prisma 8 in a shared database package inside a pnpm workspaces monorepo and query it from a Next.js app.
- [`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.