Skip to content

feat: add orm/multitenant row-level scope example#8561

Open
cancatma-hola wants to merge 1 commit into
prisma:latestfrom
cancatma-hola:feat/multitenant-row-level-scope
Open

feat: add orm/multitenant row-level scope example#8561
cancatma-hola wants to merge 1 commit into
prisma:latestfrom
cancatma-hola:feat/multitenant-row-level-scope

Conversation

@cancatma-hola

@cancatma-hola cancatma-hola commented Jul 13, 2026

Copy link
Copy Markdown

Summary

Adds a new orm/multitenant example demonstrating tenant isolation patterns for multi-tenant SaaS applications built with Prisma.

What's included

Schema:

  • TenantUser (email unique per tenant) → Order (reference unique per tenant)
  • @@index([tenantId]) on all tenant-scoped models for query performance

src/tenant-scope.ts — two composable patterns:

  1. tenantWhere(tenantId) — returns a where fragment for manual spreading:

    const orders = await prisma.order.findMany({
      where: { ...tenantWhere(tenantId), status: 'PENDING' },
    })
  2. assertTenantAccess(resource, tenantId) — guards cross-tenant reads after non-tenant key lookups:

    const order = await prisma.order.findFirst({ where: { reference } })
    assertTenantAccess(order, tenantId, 'order') // throws if wrong tenant
  3. createTenantClient(prisma, tenantId) — PrismaClient extension that auto-injects tenantId into every query on tenant-scoped models:

    const db = createTenantClient(prisma, tenantId)
    const orders = await db.order.findMany() // tenantId injected automatically

src/seed.ts — creates 2 tenants (Acme Corp, Globex Inc) with users and orders.

src/index.ts — runnable demo showing both patterns side-by-side, including the cross-tenant access prevention.

Why this example is useful

Multi-tenancy is one of the most common Prisma use cases (SaaS, B2B platforms) but there's currently no official example showing row-level tenant scoping. Developers working on multi-tenant apps frequently add tenantId to every model but need a reference pattern for enforcing the scope consistently.

The createTenantClient extension pattern is especially useful in Next.js Server Actions / middleware where you resolve tenantId once from the auth session and want all downstream Prisma calls to be automatically scoped without per-query boilerplate.

Testing

cd orm/multitenant
cp .env.example .env
# set DATABASE_URL
npm install
npm run migrate
npm run seed
npm run dev

Summary by CodeRabbit

  • New Features
    • Added a Prisma-based multi-tenant example with tenant-isolated users and orders.
    • Added manual and automatic tenant-scoping demonstrations.
    • Added seed data for two sample tenants, users, and orders.
    • Added safeguards against cross-tenant data access.
  • Documentation
    • Added setup instructions, schema details, usage examples, and security considerations.
  • Chores
    • Added configuration for PostgreSQL, migrations, TypeScript, development, and seeding.

Demonstrates two patterns for tenant isolation in Prisma:

1. Manual scoping: tenantWhere() + assertTenantAccess() helpers
   - tenantWhere(tenantId) returns a where clause fragment
   - assertTenantAccess() guards cross-tenant reads after non-tenant lookups

2. Automatic scoping: createTenantClient() PrismaClient extension
   - Injects tenantId into every query on tenant-scoped models
   - Zero per-query boilerplate in request handlers

Schema: Tenant -> User (@@unique[email,tenantId]) + Order (@@unique[reference,tenantId])
Includes seed.ts (2 tenants, realistic data) and a runnable index.ts demo.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Multi-tenant Prisma example

Layer / File(s) Summary
Schema and project setup
orm/multitenant/.env.example, orm/multitenant/package.json, orm/multitenant/tsconfig.json, orm/multitenant/prisma.config.ts, orm/multitenant/prisma/schema.prisma
Adds PostgreSQL and TypeScript configuration plus Tenant, User, and Order models with tenant-scoped constraints and order statuses.
Tenant seed data
orm/multitenant/src/seed.ts
Seeds two tenants, one user per tenant, and three tenant-scoped orders using repeatable upserts.
Tenant scoping utilities and demo
orm/multitenant/src/tenant-scope.ts, orm/multitenant/src/index.ts, orm/multitenant/README.md
Adds manual filtering, access assertions, automatic Prisma client scoping, executable demonstrations, and setup documentation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Demo as index.ts
  participant Scope as tenant-scope.ts
  participant Prisma as PrismaClient
  Demo->>Scope: createTenantClient(prisma, tenantId)
  Scope->>Prisma: Apply tenantId filters
  Demo->>Prisma: Query orders, users, and counts
  Prisma-->>Demo: Return tenant-scoped results
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a multitenant row-level scoping example under orm/multitenant.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
orm/multitenant/prisma/schema.prisma (1)

31-44: 🗄️ Data Integrity & Integration | 🔵 Trivial

No schema-level enforcement of cross-tenant referential integrity on Order.

Order has both tenantId (→ Tenant) and userId (→ User), but nothing at the schema level prevents assigning a user from tenant A to an order in tenant B. Prisma doesn't support composite foreign keys, so this must be enforced in the application layer. Ensure createTenantClient() and assertTenantAccess() in tenant-scope.ts validate that userId belongs to the same tenant before writing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@orm/multitenant/prisma/schema.prisma` around lines 31 - 44, Update
createTenantClient() and assertTenantAccess() in tenant-scope.ts to validate
that an Order’s userId belongs to the current tenant before any Order write.
Preserve the existing tenant scoping behavior and reject cross-tenant user
assignments at the application layer.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@orm/multitenant/package.json`:
- Around line 11-14: Update the PrismaClient construction in src/index.ts and
src/seed.ts to instantiate PrismaPg from `@prisma/adapter-pg` using the existing
pg connection configuration, then pass that adapter to PrismaClient. Ensure both
runtime and seed paths use the adapter, or remove the unused `@prisma/adapter-pg`
and pg dependencies if adapter wiring is not intended.

In `@orm/multitenant/prisma/schema.prisma`:
- Line 34: Update the `amount` field in the Prisma schema from `Float` to
`Decimal` so monetary values retain exact precision. Preserve the field name and
existing model structure.

In `@orm/multitenant/README.md`:
- Around line 67-71: Qualify the “What this prevents” section in the README to
list only operations actually covered by the multitenant extension. Remove or
clarify claims involving findUnique, update, delete, and upsert, and explicitly
document the unsupported operations rather than implying complete cross-tenant
protection.

In `@orm/multitenant/src/seed.ts`:
- Around line 74-76: Update the main() promise chain so seed failures remain
rejected or explicitly set a nonzero process exit status after logging the
error; do not let catch(console.error) resolve successfully. Preserve the
prisma.$disconnect() cleanup in finally while ensuring failed seeding exits with
a nonzero code.

In `@orm/multitenant/src/tenant-scope.ts`:
- Around line 51-96: The createTenantClient extension has incomplete Prisma
operation coverage, allowing unscoped cross-tenant access. In
orm/multitenant/src/tenant-scope.ts lines 51-96, add tenant-scoped handlers for
findUnique, findUniqueOrThrow, findFirstOrThrow, update, upsert, delete,
createMany, aggregate, and groupBy on both order and user, plus count,
updateMany, and deleteMany on user, preserving tenantId injection for reads,
writes, and filters. In orm/multitenant/README.md lines 67-71, keep the existing
protection claims now that coverage is complete, or explicitly document any
intentionally unsupported operations.

---

Nitpick comments:
In `@orm/multitenant/prisma/schema.prisma`:
- Around line 31-44: Update createTenantClient() and assertTenantAccess() in
tenant-scope.ts to validate that an Order’s userId belongs to the current tenant
before any Order write. Preserve the existing tenant scoping behavior and reject
cross-tenant user assignments at the application layer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bfa749be-5821-4f10-a9f1-29259e42164b

📥 Commits

Reviewing files that changed from the base of the PR and between 6d44921 and 822271c.

📒 Files selected for processing (9)
  • orm/multitenant/.env.example
  • orm/multitenant/README.md
  • orm/multitenant/package.json
  • orm/multitenant/prisma.config.ts
  • orm/multitenant/prisma/schema.prisma
  • orm/multitenant/src/index.ts
  • orm/multitenant/src/seed.ts
  • orm/multitenant/src/tenant-scope.ts
  • orm/multitenant/tsconfig.json

Comment on lines +11 to +14
"@prisma/adapter-pg": "7.0.0",
"@prisma/client": "7.0.0",
"dotenv": "16.6.1",
"pg": "8.20.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if Prisma 7.0.0 exists on npm and inspect its exports
npm view prisma@7.0.0 version 2>/dev/null
npm view `@prisma/client`@7.0.0 version 2>/dev/null
npm view `@prisma/adapter-pg`@7.0.0 version 2>/dev/null
npm view `@types/pg`@8.20.0 version 2>/dev/null
npm view pg@8.20.0 version 2>/dev/null

Repository: prisma/prisma-examples

Length of output: 192


🌐 Web query:

What is the latest version of Prisma ORM and does Prisma 7.0.0 exist? What is the prisma/config module API (defineConfig, env)? Does Prisma 7 use "prisma-client" as the generator provider instead of "prisma-client-js"? Is @prisma/adapter-pg auto-configured when installed as a dependency?

💡 Result:

As of July 13, 2026, the latest version of Prisma ORM is 7.8.0 [1][2][3]. Prisma 7.0.0 exists; it was released on November 19, 2025, and introduced the Rust-free Prisma Client as the default [4][5][6]. The prisma/config module provides an API to manage project configuration, including the defineConfig helper and env function [7][8]. The defineConfig helper is used to structure the configuration object (such as schema paths and datasource settings), while env() is used to safely access environment variables within that configuration file (e.g., prisma.config.ts) [7][9]. Regarding the generator provider, Prisma 7 uses prisma-client as the default generator provider [10][11]. The older prisma-client-js provider is deprecated as of Prisma 7 [10][12]. When using the prisma-client provider, the output field in the generator block is required, as the client is no longer generated inside node_modules by default [13][10][12]. The @prisma/adapter-pg package (and other driver adapters) is not auto-configured when installed as a dependency [14][15][16]. To use it, you must explicitly install the adapter and instantiate the PrismaClient by passing an instance of the adapter to the constructor, as shown in the following example [14][16][17]: import { PrismaClient } from "../generated/prisma/client"; import { PrismaPg } from "@prisma/adapter-pg"; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); const prisma = new PrismaClient({ adapter });

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files 'orm/multitenant/*' | sort

printf '\n== package.json ==\n'
cat -n orm/multitenant/package.json

printf '\n== schema/prisma config ==\n'
for f in orm/multitenant/prisma.config.ts orm/multitenant/prisma/schema.prisma orm/multitenant/seed.ts orm/multitenant/index.ts; do
  if [ -f "$f" ]; then
    printf '\n--- %s ---\n' "$f"
    cat -n "$f"
  fi
done

Repository: prisma/prisma-examples

Length of output: 2969


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== search for PrismaClient / adapter-pg usage ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'PrismaClient|adapter-pg|PrismaPg|defineConfig|prisma-client-js|prisma-client' orm/multitenant

Repository: prisma/prisma-examples

Length of output: 1527


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json, subprocess, sys
for pkg in ["prisma@7.0.0", "`@prisma/client`@7.0.0", "`@prisma/adapter-pg`@7.0.0"]:
    try:
        out = subprocess.check_output(["npm","view",pkg,"peerDependencies","dependencies","exports","type","version"], stderr=subprocess.DEVNULL, text=True)
        print(f"\n## {pkg}\n{out}")
    except subprocess.CalledProcessError as e:
        print(f"\n## {pkg}\nERROR")
PY

Repository: prisma/prisma-examples

Length of output: 3660


🌐 Web query:

Prisma 7 provider = "prisma-client" new PrismaClient()without adapter. Does it connect to PostgreSQL automatically, or must@prisma/adapter-pg be passed to the constructor?

💡 Result:

In Prisma 7, you cannot connect to a PostgreSQL database using new PrismaClient() without providing an adapter [1][2]. Prisma 7 requires a driver adapter to be passed to the PrismaClient constructor [3][4]. If you attempt to instantiate new PrismaClient() without an adapter (or accelerateUrl), it will result in an error [1][4]. To connect to PostgreSQL, you must explicitly install and provide the @prisma/adapter-pg driver adapter [5][1]. The standard configuration for a Node.js environment involves the following steps: 1. Install the necessary dependencies: npm install @prisma/client @prisma/adapter-pg pg [1]. 2. Configure and pass the adapter to the constructor [5][6]: ts import { PrismaClient } from "./generated/prisma/client"; import { PrismaPg } from "`@prisma/adapter-pg`"; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL!, }); export const prisma = new PrismaClient({ adapter }); This change is part of Prisma 7's move toward a "Rust-free" Prisma Client, which requires developers to provide the specific database drivers directly in their project code instead of having them embedded within the Prisma binary [5].

Citations:


Pass the Postgres adapter into PrismaClient
The Prisma 7 config is fine, but @prisma/adapter-pg and pg are still unused: src/index.ts and src/seed.ts both call new PrismaClient() without an adapter. Prisma 7 needs PrismaPg passed to the client for PostgreSQL, so either wire that in or remove the adapter deps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@orm/multitenant/package.json` around lines 11 - 14, Update the PrismaClient
construction in src/index.ts and src/seed.ts to instantiate PrismaPg from
`@prisma/adapter-pg` using the existing pg connection configuration, then pass
that adapter to PrismaClient. Ensure both runtime and seed paths use the
adapter, or remove the unused `@prisma/adapter-pg` and pg dependencies if adapter
wiring is not intended.

model Order {
id String @id @default(cuid())
reference String
amount Float

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use Decimal instead of Float for monetary amounts.

Float can introduce floating-point precision errors for monetary values. Decimal is the recommended Prisma type for financial data.

🛡️ Proposed fix
 model Order {
   id        String      `@id` `@default`(cuid())
   reference String
-  amount    Float
+  amount    Decimal `@db.Decimal`(12, 2)
   status    OrderStatus `@default`(PENDING)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
amount Float
amount Decimal `@db.Decimal`(12, 2)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@orm/multitenant/prisma/schema.prisma` at line 34, Update the `amount` field
in the Prisma schema from `Float` to `Decimal` so monetary values retain exact
precision. Preserve the field name and existing model structure.

Comment thread orm/multitenant/README.md
Comment on lines +67 to +71
## What this prevents

- Cross-tenant data reads (a user from Tenant A fetching Tenant B's orders)
- Missing `WHERE tenantId = ?` clauses that return all-tenants data
- Silent cross-tenant leaks via non-tenant lookup keys (e.g. `findUnique({ where: { id } })`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

"What this prevents" section overstates the extension's coverage.

The extension doesn't cover findUnique, update, delete, or upsert, so the claims about preventing cross-tenant reads and silent leaks via findUnique({ where: { id } }) are inaccurate. Either add the missing operations to the extension (see tenant-scope.ts comment) or qualify this section to document which operations are and aren't covered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@orm/multitenant/README.md` around lines 67 - 71, Qualify the “What this
prevents” section in the README to list only operations actually covered by the
multitenant extension. Remove or clarify claims involving findUnique, update,
delete, and upsert, and explicitly document the unsupported operations rather
than implying complete cross-tenant protection.

Comment on lines +74 to +76
main()
.catch(console.error)
.finally(() => prisma.$disconnect())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Seed failures exit with code 0 due to swallowed rejection.

.catch(console.error) returns undefined, resolving the promise chain. The process exits with code 0 even when seeding fails, which hides failures in CI pipelines.

🛡️ Proposed fix
 main()
-  .catch(console.error)
+  .catch((error) => {
+    console.error(error)
+    process.exitCode = 1
+  })
   .finally(() => prisma.$disconnect())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
main()
.catch(console.error)
.finally(() => prisma.$disconnect())
main()
.catch((error) => {
console.error(error)
process.exitCode = 1
})
.finally(() => prisma.$disconnect())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@orm/multitenant/src/seed.ts` around lines 74 - 76, Update the main() promise
chain so seed failures remain rejected or explicitly set a nonzero process exit
status after logging the error; do not let catch(console.error) resolve
successfully. Preserve the prisma.$disconnect() cleanup in finally while
ensuring failed seeding exits with a nonzero code.

Comment on lines +51 to +96
export function createTenantClient(prisma: PrismaClient, tenantId: string) {
return prisma.$extends({
query: {
order: {
async findMany({ args, query }) {
args.where = { ...args.where, tenantId }
return query(args)
},
async findFirst({ args, query }) {
args.where = { ...args.where, tenantId }
return query(args)
},
async count({ args, query }) {
args.where = { ...args.where, tenantId }
return query(args)
},
async create({ args, query }) {
args.data = { ...args.data, tenantId }
return query(args)
},
async updateMany({ args, query }) {
args.where = { ...args.where, tenantId }
return query(args)
},
async deleteMany({ args, query }) {
args.where = { ...args.where, tenantId }
return query(args)
},
},
user: {
async findMany({ args, query }) {
args.where = { ...args.where, tenantId }
return query(args)
},
async findFirst({ args, query }) {
args.where = { ...args.where, tenantId }
return query(args)
},
async create({ args, query }) {
args.data = { ...args.data, tenantId }
return query(args)
},
},
},
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Incomplete operation coverage in createTenantClient leaves cross-tenant gaps; README overstates protection. The extension only overrides a subset of Prisma operations, so findUnique, update, delete, and upsert bypass tenant scoping entirely. The README's "What this prevents" section claims to block findUnique leaks, but the extension doesn't cover findUnique.

  • orm/multitenant/src/tenant-scope.ts#L51-L96: Add findUnique, findUniqueOrThrow, findFirstOrThrow, update, upsert, delete, createMany, aggregate, and groupBy for both order and user; add count, updateMany, and deleteMany for user. See the per-site comment for a full diff.
  • orm/multitenant/README.md#L67-L71: After adding the missing operations, the current claims become accurate. Alternatively, if some operations are intentionally excluded, document the limitations here.
📍 Affects 2 files
  • orm/multitenant/src/tenant-scope.ts#L51-L96 (this comment)
  • orm/multitenant/README.md#L67-L71
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@orm/multitenant/src/tenant-scope.ts` around lines 51 - 96, The
createTenantClient extension has incomplete Prisma operation coverage, allowing
unscoped cross-tenant access. In orm/multitenant/src/tenant-scope.ts lines
51-96, add tenant-scoped handlers for findUnique, findUniqueOrThrow,
findFirstOrThrow, update, upsert, delete, createMany, aggregate, and groupBy on
both order and user, plus count, updateMany, and deleteMany on user, preserving
tenantId injection for reads, writes, and filters. In orm/multitenant/README.md
lines 67-71, keep the existing protection claims now that coverage is complete,
or explicitly document any intentionally unsupported operations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant