Multi-tenancy
Tenant isolation is enforced by Postgres Row-Level Security, not application-level filtering.
Every tenant-scoped table is isolated at the database layer, not by remembering to add
WHERE tenantId = ... to every query. This is the single most important pattern in the backend to
understand before touching data access.
How it's wired
prisma/setup-db.tscreates a non-superuserperagraph_approle and, for every tenant-scoped table, atenant_isolationRLS policy:tenantId = current_setting('app.current_tenant_id'). The runtime connects as this restricted role, never as a superuser.nestjs-clscarries the currenttenantIdthrough the request viaAsyncLocalStorage.JwtAuthGuardsets it once per HTTP request from the JWT payload; BullMQ job handlers set it once per job.src/database/tenant-prisma.tswraps the base Prisma client in a$extendsthat intercepts every model operation: it opens a transaction, runsSELECT set_config('app.current_tenant_id', ...), then dispatches the real query inside that transaction. Every service that needs tenant-scoped data is injected this wrapped client, never the raw one.
Where it doesn't apply, on purpose
Two categories of code intentionally bypass the tenant-scoped extension, and both have to open
their own set_config call manually if they touch tenant data at all:
- Raw SQL.
dedup.service.tsandscheduler.service.tsrun$queryRaw/$executeRawdirectly, which skips the$extendsinterceptor entirely. - Boot-time cross-tenant scans.
SchedulerServiceuses a separate superuser connection (DIRECT_DATABASE_URL) at startup to scan recurring configs across every tenant at once: there is no request context yet to scope a single tenant from, since nothing has been requested.
Every tenant-facing service also calls a tenantId() helper that reads from ClsService and
throws NotFoundException('No tenant context') if it's absent. This is a deliberate fail-closed
guard, not a formality: an absent tenant context should never silently resolve to "no filter," it
should refuse to run.
Testing it
test/rls-isolation.int-spec.ts runs against a live database and is the test to run if you touch
anything in this area: it's the thing that actually proves one tenant can't read another's rows,
rather than trusting the policy definitions by inspection.