Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

Multiple databases

This guide shows you how to use two databases from one Next.js app with Prisma ORM. You give each database its own data contract, its own prisma.config.ts, and its own Prisma ORM client, then render data from both on one page. The same layout works for any number of databases and is a good fit for multi-tenant apps or for keeping unrelated data in separate databases.

Every command and code block below was run against two local PostgreSQL databases, users and posts.

  • Node.js 24 or later
  • Two PostgreSQL connection strings, or nothing at all: npx create-db@latest can create a Prisma Postgres database for you, run it twice for two
  • A Vercel account if you want to deploy at the end

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

with
Create a Next.js app that reads from two PostgreSQL databases with Prisma ORM, following https://www.prisma.io/docs/guides/database/multiple-databases.md.

1. Scaffold with `npx create-next-app@latest my-multi-db-app --yes`, then in the project run `npx prisma@latest orm init --yes --target postgres --authoring psl --schema-path ./prisma/users/contract.prisma`. Run `npx prisma@latest init` so the Prisma agent skills are installed, and use them.

2. Rename `prisma.config.ts` to `prisma.users.config.ts`, point it at `USERS_DATABASE_URL` and `./prisma/users/migrations`, and reduce `prisma/users/contract.prisma` to a `User` model. Create the same three files for the posts database: `prisma.posts.config.ts` reading `POSTS_DATABASE_URL`, `prisma/posts/contract.prisma` with a `Post` model that stores `authorId Int` (no relation across databases), and `prisma/posts/db.ts`. Export `usersDb` and `postsDb` from the two `db.ts` files.

3. Put both connection strings in `.env` (use the ones I give you, or create two Prisma Postgres databases with `npx create-db@latest` and show me the claim URLs). Add package scripts that run `prisma contract emit`, `prisma db init`, and `prisma db verify` once per config with `--config`, then run them.

4. Write a seed script that inserts two users and two posts, run it with `node prisma/seed.ts`, and rewrite `app/page.tsx` as a server component that lists users from `usersDb` and posts from `postsDb`, joining authors in application code.

5. Start `npm run dev` in the background, verify http://localhost:3000 renders both lists, stop the server, and confirm `npm run build` passes.

Create a new Next.js app and accept the defaults (TypeScript, Tailwind CSS, App Router, no src directory):

bunx create-next-app@latest my-multi-db-app

cd my-multi-db-app
Bash
pnpm dlx create-next-app@latest my-multi-db-app
cd my-multi-db-app
Bash
yarn dlx create-next-app@latest my-multi-db-app
cd my-multi-db-app
Bash
npx create-next-app@latest my-multi-db-app
cd my-multi-db-app

This guide starts from create-next-app and adds Prisma ORM with orm init instead of using the next template of create-prisma. That template wires a single database and declares it for Prisma Compute; here you want exactly the Prisma files, once per database, and nothing else.

This guide starts from create-next-app and adds Prisma ORM with orm init instead of using the next template of create-prisma. That template wires a single database and declares it for Prisma Compute; here you want exactly the Prisma files, once per database, and nothing else.

Run orm init from the project root. The --schema-path flag puts the contract in a folder named after the database, and orm init places the client file next to it:

bunx prisma@latest orm init --yes --target postgres --authoring psl --schema-path ./prisma/users/contract.prisma
Bash
pnpm dlx prisma@latest orm init --yes --target postgres --authoring psl --schema-path ./prisma/users/contract.prisma
Bash
yarn dlx prisma@latest orm init --yes --target postgres --authoring psl --schema-path ./prisma/users/contract.prisma
Bash
npx prisma@latest orm init --yes --target postgres --authoring psl --schema-path ./prisma/users/contract.prisma

The command installs @prisma/orm-postgres and dotenv, adds prisma as a dev dependency, sets "type": "module" in package.json, updates tsconfig.json, and writes these files:

no-copy
prisma/users/contract.prisma
prisma/users/db.ts
prisma.config.ts
prisma-8.md
.env.example

There is no prisma generate and no generated client package. The emitted contract.json and contract.d.ts next to the contract are what the runtime and the type checker read.

The command installs @prisma/orm-postgres and dotenv, adds prisma as a dev dependency, sets "type": "module" in package.json, updates tsconfig.json, and writes these files:

prisma/users/contract.prisma

prisma/users/db.ts

prisma.config.ts

prisma-8.md

.env.example

There is no prisma generate and no generated client package. The emitted contract.json and contract.d.ts next to the contract are what the runtime and the type checker read.

Prisma ORM commands read ./prisma.config.ts by default and take a different file with the global --config flag. Rename the generated config so each database has one:

mv prisma.config.ts prisma.users.config.ts

Point it at the users contract, a users-only migrations folder, and a USERS_DATABASE_URL variable:

title="prisma.users.config.ts"
import 'dotenv/config';

import { definePrismaConfig } from '@prisma/cli-engine';

import { defineConfig as ormConfig } from '@prisma/orm-postgres/config';

export default definePrismaConfig({

  orm: ormConfig({

    contract: "./prisma/users/contract.prisma",

    migrations: { 

      dir: "./prisma/users/migrations", 

    }, 

    db: {

      connection: process.env.USERS_DATABASE_URL!, 

    },

  }),

});

migrations.dir matters once you use checked-in migrations: without it both configs would write to the same ./migrations folder.

Replace the starter contract with a single User model:

title="prisma/users/contract.prisma"
// use prisma-8

model User {

  id        Int               @id @default(autoincrement())

  email     String            @unique

  name      String?

  createdAt TimestamptzString @default(now())

}

Change prisma/users/db.ts to read USERS_DATABASE_URL and export a name you can tell apart from the second client:

title="prisma/users/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 usersDb = postgres<Contract>({ 

  contractJson,

  url: process.env.USERS_DATABASE_URL!, 

});

contract.d.ts and contract.json do not exist yet; you emit them in step 4.

The second database needs the same three files. Create them by hand; running orm init again would replace the files from step 2.

mkdir -p prisma/posts
title="prisma.posts.config.ts"
import 'dotenv/config';

import { definePrismaConfig } from '@prisma/cli-engine';

import { defineConfig as ormConfig } from '@prisma/orm-postgres/config';

export default definePrismaConfig({

  orm: ormConfig({

    contract: "./prisma/posts/contract.prisma",

    migrations: {

      dir: "./prisma/posts/migrations",

    },

    db: {

      connection: process.env.POSTS_DATABASE_URL!,

    },

  }),

});
title="prisma/posts/contract.prisma"
// use prisma-8

model Post {

  id        Int               @id @default(autoincrement())

  title     String

  content   String?

  authorId  Int

  createdAt TimestamptzString @default(now())

}

authorId is a plain integer, not a relation. A contract describes one database, and PostgreSQL cannot enforce a foreign key into another database, so the link between a post and its author is resolved in application code in step 5.

title="prisma/posts/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 postsDb = postgres<Contract>({

  contractJson,

  url: process.env.POSTS_DATABASE_URL!,

});

Now set both connection strings. Both config files and both clients load .env through dotenv/config, and Next.js loads it as well:

title=".env"
USERS_DATABASE_URL="postgres://user:password@localhost:5432/users"

POSTS_DATABASE_URL="postgres://user:password@localhost:5432/posts"

orm init already added .env to .gitignore. Update .env.example with the two variable names so collaborators know what to set.

Each Prisma ORM command works on one config. Add scripts that run every command once per database so you do not have to type --config twice:

title="package.json"
"scripts": {

  "dev": "next dev",

  "build": "next build",

  "start": "next start",

  "contract:emit": "prisma contract emit --config ./prisma.users.config.ts && prisma contract emit --config ./prisma.posts.config.ts", 

  "db:init": "prisma db init --config ./prisma.users.config.ts && prisma db init --config ./prisma.posts.config.ts", 

  "db:verify": "prisma db verify --config ./prisma.users.config.ts && prisma db verify --config ./prisma.posts.config.ts", 

  "db:update": "prisma db update --config ./prisma.users.config.ts && prisma db update --config ./prisma.posts.config.ts", 

  "db:seed": "node prisma/seed.ts", 

  "prebuild": "npm run contract:emit"

}

prebuild re-emits both contracts before every next build, locally and on Vercel, so the build never runs against stale artifacts.

Emit the contract artifacts for both databases. This step is offline:

bun run contract:emit
Bash
pnpm run contract:emit
Bash
yarn contract:emit
Bash
npm run contract:emit
no-copy
✔ Emitted contract.json and contract.d.ts
│  contract:  prisma/users/contract.json
│  types:     prisma/users/contract.d.ts

✔ Emitted contract.json and contract.d.ts
│  contract:  prisma/posts/contract.json
│  types:     prisma/posts/contract.d.ts

Create the tables in both databases and sign each one with its contract:

✔ Emitted contract.json and contract.d.ts

│  contract:  prisma/users/contract.json

│  types:     prisma/users/contract.d.ts

✔ Emitted contract.json and contract.d.ts

│  contract:  prisma/posts/contract.json

│  types:     prisma/posts/contract.d.ts

Create the tables in both databases and sign each one with its contract:

bun run db:init
Bash
pnpm run db:init
Bash
yarn db:init
Bash
npm run db:init
no-copy
│  contract:  prisma/users/contract.json
│  database:  postgres://****@localhost:5432/users

✔ Applied 2 operation(s) across 1 contract space

App space
├─ Create table "User"
├─ Add unique constraint on "User" (email)
└─ marker f340195186d0acf8e9914097e58af0375650e7251358382aa9ad3ed5b724daf7

│  contract:  prisma/posts/contract.json
│  database:  postgres://****@localhost:5432/posts

✔ Applied 1 operation(s) across 1 contract space

App space
├─ Create table "Post"
└─ marker a0c012793cc392569087d82d7ea41686c8e60586b63c7a131391b99717157804

Each database now carries a marker with the hash of the contract it was initialized from. Confirm both match:

│  contract:  prisma/users/contract.json

│  database:  postgres://****@localhost:5432/users

✔ Applied 2 operation(s) across 1 contract space

App space

├─ Create table "User"

├─ Add unique constraint on "User" (email)

└─ marker f340195186d0acf8e9914097e58af0375650e7251358382aa9ad3ed5b724daf7

│  contract:  prisma/posts/contract.json

│  database:  postgres://****@localhost:5432/posts

✔ Applied 1 operation(s) across 1 contract space

App space

├─ Create table "Post"

└─ marker a0c012793cc392569087d82d7ea41686c8e60586b63c7a131391b99717157804

Each database now carries a marker with the hash of the contract it was initialized from. Confirm both match:

bun run db:verify
Bash
pnpm run db:verify
Bash
yarn db:verify
Bash
npm run db:verify
no-copy
│  contract:  prisma/users/contract.json
│  database:  postgres://****@localhost:5432/users

✔ Database marker and schema match contract

│  contract:  prisma/posts/contract.json
│  database:  postgres://****@localhost:5432/posts

✔ Database marker and schema match contract

The marker is also what protects you from crossing the wires. Verifying the users contract against the posts database stops immediately:

│  contract:  prisma/users/contract.json

│  database:  postgres://****@localhost:5432/users

✔ Database marker and schema match contract

│  contract:  prisma/posts/contract.json

│  database:  postgres://****@localhost:5432/posts

✔ Database marker and schema match contract

The marker is also what protects you from crossing the wires. Verifying the users contract against the posts database stops immediately:

bunx prisma db verify --config ./prisma.users.config.ts --db "$POSTS_DATABASE_URL"
Bash
pnpm prisma db verify --config ./prisma.users.config.ts --db "$POSTS_DATABASE_URL"
Bash
yarn prisma db verify --config ./prisma.users.config.ts --db "$POSTS_DATABASE_URL"
Bash
npx prisma db verify --config ./prisma.users.config.ts --db "$POSTS_DATABASE_URL"
no-copy
✘ [CONTRACT.MARKER_MISMATCH] Hash mismatch
  why: Contract storageHash does not match database marker
✘ [CONTRACT.MARKER_MISMATCH] Hash mismatch

  why: Contract storageHash does not match database marker

Create a script that writes through both clients. Node.js 24 runs TypeScript directly, so no extra tooling is needed:

title="prisma/seed.ts"
import { usersDb } from "./users/db.ts";

import { postsDb } from "./posts/db.ts";

async function main() {

  const alice = await usersDb.orm.public.User.create({

    email: "alice@prisma.io",

    name: "Alice",

  });

  const bob = await usersDb.orm.public.User.create({

    email: "bob@prisma.io",

    name: "Bob",

  });

  await postsDb.orm.public.Post.create({

    title: "Hello from the posts database",

    content: "This row lives in the posts database.",

    authorId: alice.id,

  });

  await postsDb.orm.public.Post.create({

    title: "Two databases, one app",

    content: null,

    authorId: bob.id,

  });

  console.log("Seeded", alice.email, "and", bob.email, "with one post each.");

  await usersDb.close();

  await postsDb.close();

}

main().catch((error) => {

  console.error(error);

  process.exit(1);

});

The .ts extensions in the imports are what Node.js needs to resolve the files. Allow them in tsconfig.json, otherwise next build fails its type check with TS5097:

title="tsconfig.json"
{

  "compilerOptions": {

    "allowImportingTsExtensions": true

  }

}

Run the seed:

bun run db:seed
Bash
pnpm run db:seed
Bash
yarn db:seed
Bash
npm run db:seed
no-copy
Seeded alice@prisma.io and bob@prisma.io with one post each.

Both clients open their connection pool on the first query. The script closes them at the end so the process can exit.

Seeded alice@prisma.io and bob@prisma.io with one post each.

Both clients open their connection pool on the first query. The script closes them at the end so the process can exit.

Replace app/page.tsx with a server component that queries each client and joins posts to their authors in memory:

title="app/page.tsx"
import { usersDb } from "@/prisma/users/db";

import { postsDb } from "@/prisma/posts/db";

export const dynamic = "force-dynamic";

export default async function Home() {

  const users = await usersDb.orm.public.User.select("id", "email", "name").all();

  const posts = await postsDb.orm.public.Post

    .select("id", "title", "authorId")

    .orderBy((p) => p.id.asc())

    .all();

  const authorById = new Map(users.map((u) => [u.id, u]));

  return (

    <main className="mx-auto max-w-2xl p-8 font-sans">

      <h1 className="text-3xl font-bold">Multi-database showcase</h1>

      <p className="mt-2 text-zinc-600">

        Users come from one PostgreSQL database, posts from another.

      </p>

      <h2 className="mt-8 text-xl font-semibold">Users</h2>

      <ul className="mt-2 list-disc pl-6">

        {users.map((user) => (

          <li key={user.id}>

            {user.name} ({user.email})

          </li>

        ))}

      </ul>

      <h2 className="mt-8 text-xl font-semibold">Posts</h2>

      <ul className="mt-2 list-disc pl-6">

        {posts.map((post) => (

          <li key={post.id}>

            {post.title}, by {authorById.get(post.authorId)?.name ?? "unknown"}

          </li>

        ))}

      </ul>

    </main>

  );

}

Each client is typed by its own contract.d.ts: usersDb.orm.public only knows User, postsDb.orm.public only knows Post, and mixing them up is a type error. force-dynamic keeps Next.js from trying to render the page at build time, when no database is reachable.

bun run dev
Bash
pnpm run dev
Bash
yarn dev
Bash
npm run dev
no-copy
▲ Next.js 16.3.4 (Turbopack)
- Local:         http://localhost:3000
- Environments: .env
✓ Ready in 894ms

Open http://localhost:3000. The page lists Alice and Bob under Users and the two posts under Posts, each attributed to its author. One request, two databases, no adapter or extra client package in between.

Finally, confirm the production build passes. prebuild emits both contracts first:

▲ Next.js 16.3.4 (Turbopack)

- Local:         http://localhost:3000

- Environments: .env

✓ Ready in 894ms

Open http://localhost. The page lists Alice and Bob under Users and the two posts under Posts, each attributed to its author. One request, two databases, no adapter or extra client package in between.

Finally, confirm the production build passes. prebuild emits both contracts first:

bun run build
Bash
pnpm run build
Bash
yarn build
Bash
npm run build
no-copy
> prisma contract emit --config ./prisma.users.config.ts && prisma contract emit --config ./prisma.posts.config.ts
...
✓ Compiled successfully
> prisma contract emit --config ./prisma.users.config.ts && prisma contract emit --config ./prisma.posts.config.ts

...

✓ Compiled successfully

The app needs two environment variables in production and nothing else that is Prisma-specific: prebuild runs on Vercel, and there is no client to generate and no engine binary to ship.

  1. Push the project to a GitHub repository. If you do not have one yet, create one on GitHub, then run:

    git add .
    
    git commit -m "Next.js app with two Prisma 8 databases"
    
    git branch -M main
    
    git remote add origin https://github.com/<your-username>/<repository-name>.git
    
    git push -u origin main
  2. In the Vercel dashboard, follow Import an existing project and stop at the step where you configure the project, before clicking Deploy.

  3. Expand Environment variables and add both connection strings:

    • Key: USERS_DATABASE_URL, Value: the users database connection string from your .env
    • Key: POSTS_DATABASE_URL, Value: the posts database connection string from your .env
  4. Click Deploy. Vercel runs npm run build, which emits both contracts and builds the app.

Open the live URL. The page renders the same two lists from the same two databases. The Vercel deploy was not run while validating this guide; the npm run build it executes is the one from step 5.

  • No relations across databases. A contract covers one database. Keep a plain id column such as authorId on the side that references the other database, and join in application code.
  • npx prisma@latest init writes a third config. It creates a prisma.config.ts that holds only the skills section for agent skills. That is expected; the database commands keep using --config with the two database configs.
  • Do not close a client per request. In page.tsx and route handlers, never call usersDb.close(). The pools are shared across requests and close when the process exits. Only scripts such as prisma/seed.ts close them.
  • orm init picks the package manager from your lockfile. Run it after create-next-app has written package-lock.json, pnpm-lock.yaml, or bun.lock, or it may install with a different package manager than the one you use.

Run npx prisma@latest init once to install the Prisma ORM skills for your coding agent and keep them matching your installed packages. Prompts that map to this guide:

  • "Using the prisma-8 skill, add a Comment model to prisma/posts/contract.prisma, emit with --config ./prisma.posts.config.ts, and update the posts database."
  • "Add a third database for billing following the same layout: prisma.billing.config.ts, prisma/billing/contract.prisma, prisma/billing/db.ts, and extend the package scripts."
  • "Write a route handler that creates a post for the signed-in user, validating that the authorId exists in the users database first."
  • Change a contract, then run npm run contract:emit and npm run db:update to apply the change to both databases in development. For checked-in migrations, run migration plan and db migrate with the --config of the database you changed; each writes to its own migrations.dir.
  • Learn the fundamentals: filtering, sorting, pagination, and writes.
  • CLI configuration covers everything prisma.config.ts can hold.
  • Splitting the app itself into packages? See pnpm workspaces and Turborepo.
Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu