feat: add orm/multitenant row-level scope example#8561
Conversation
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.
WalkthroughChangesMulti-tenant Prisma example
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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
orm/multitenant/prisma/schema.prisma (1)
31-44: 🗄️ Data Integrity & Integration | 🔵 TrivialNo schema-level enforcement of cross-tenant referential integrity on
Order.
Orderhas bothtenantId(→ Tenant) anduserId(→ 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. EnsurecreateTenantClient()andassertTenantAccess()intenant-scope.tsvalidate thatuserIdbelongs 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
📒 Files selected for processing (9)
orm/multitenant/.env.exampleorm/multitenant/README.mdorm/multitenant/package.jsonorm/multitenant/prisma.config.tsorm/multitenant/prisma/schema.prismaorm/multitenant/src/index.tsorm/multitenant/src/seed.tsorm/multitenant/src/tenant-scope.tsorm/multitenant/tsconfig.json
| "@prisma/adapter-pg": "7.0.0", | ||
| "@prisma/client": "7.0.0", | ||
| "dotenv": "16.6.1", | ||
| "pg": "8.20.0" |
There was a problem hiding this comment.
🎯 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/nullRepository: 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:
- 1: https://www.npmjs.com/package/prisma
- 2: https://registry.npmjs.org/prisma
- 3: https://npmx.dev/package-changelog/@prisma/client/v/7.8.0
- 4: https://github.com/prisma/prisma/releases/tag/7.0.0
- 5: https://www.prisma.io/changelog/2025-11-19
- 6: https://www.prisma.io/blog/announcing-prisma-orm-7-0-0
- 7: https://docs.prisma.io/docs/orm/reference/prisma-config-reference
- 8: https://www.prisma.io/docs/orm/v6/reference/prisma-config-reference
- 9: https://www.prisma.io/docs/orm/reference/prisma-schema-reference
- 10: https://www.prisma.io/docs/orm/v6/prisma-schema/overview/generators
- 11: fix(cli-generator): outdated default generator provider prisma#29089
- 12: https://github.com/prisma/docs/blob/main/apps/docs/content/docs/guides/upgrade-prisma-orm/v7.mdx
- 13: https://www.prisma.io/docs/guides/upgrade-prisma-orm/v7
- 14: https://www.prisma.io/docs/v6/orm/overview/databases/postgresql
- 15: https://github.com/prisma/skills/blob/HEAD/prisma-database-setup/SKILL.md
- 16: https://www.prismagraphql.com/docs/orm/core-concepts/supported-databases/database-drivers
- 17: https://docs.prisma.io/docs/orm/v6/overview/databases/postgresql
🏁 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
doneRepository: 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/multitenantRepository: 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")
PYRepository: 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:
- 1: https://github.com/prisma/docs/blob/main/apps/docs/content/docs/orm/prisma-client/setup-and-configuration/introduction.mdx
- 2: https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/introduction
- 3: https://www.prisma.io/docs/guides/upgrade-prisma-orm/v7
- 4: https://github.com/prisma/prisma/releases/tag/7.0.0
- 5: https://www.prisma.io/changelog/2025-11-19
- 6: https://www.prisma.io/docs/orm/core-concepts/supported-databases/postgresql
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 |
There was a problem hiding this comment.
🗄️ 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.
| 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.
| ## 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 } })`) |
There was a problem hiding this comment.
🔒 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.
| main() | ||
| .catch(console.error) | ||
| .finally(() => prisma.$disconnect()) |
There was a problem hiding this comment.
🩺 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.
| 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.
| 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) | ||
| }, | ||
| }, | ||
| }, | ||
| }) | ||
| } |
There was a problem hiding this comment.
🔒 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: AddfindUnique,findUniqueOrThrow,findFirstOrThrow,update,upsert,delete,createMany,aggregate, andgroupByfor bothorderanduser; addcount,updateMany, anddeleteManyforuser. 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.
Summary
Adds a new
orm/multitenantexample demonstrating tenant isolation patterns for multi-tenant SaaS applications built with Prisma.What's included
Schema:
Tenant→User(email unique per tenant) →Order(reference unique per tenant)@@index([tenantId])on all tenant-scoped models for query performancesrc/tenant-scope.ts— two composable patterns:tenantWhere(tenantId)— returns awherefragment for manual spreading:assertTenantAccess(resource, tenantId)— guards cross-tenant reads after non-tenant key lookups:createTenantClient(prisma, tenantId)— PrismaClient extension that auto-injectstenantIdinto every query on tenant-scoped models: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
tenantIdto every model but need a reference pattern for enforcing the scope consistently.The
createTenantClientextension pattern is especially useful in Next.js Server Actions / middleware where you resolvetenantIdonce from the auth session and want all downstream Prisma calls to be automatically scoped without per-query boilerplate.Testing
Summary by CodeRabbit