useFactory is employed because we want to inject the extended prisma client returned by calling prisma.$extend(...), and you need useFactory for the sort of dynamic provider that's necessary to do this.
More importantly, creating the extended prisma client with a Custom Factory Provider means the NestJS Dependency Injection system can be utilized when creating the extended client.
This does not create mutliple prisma clients (new PrismaClient(...)) or multiple database connections per tenant or per request, as extended clients share the main client's connection. From the Prisma docs,
An extended client is a lightweight variant of the standard Prisma Client that is wrapped by one or more extensions. The standard client is not mutated. You can add as many extended clients as you want to your project.
....
Each extended client operates independently in an isolated instance.
Extended clients cannot conflict with each other, or with the standard client.
All extended clients and the standard client communicate with the same Prisma query engine.
All extended clients and the standard client share the same connection pool.
If the extended client provider is NOT request-scoped it will should only create a single extended client that will be used for the lifetime of the server process. If the useFactory provider becomes request-scoped in your particular application (somehow), the useFactory function will run on every request. However, this isn't really a problem (extended prisma clients are meant to be disposable).
This single extended prisma client works for all tenants through the use of AsyncLocalStorage, which is facilitated by nestjs-cls, to create a unique context for each request. From the Node docs:
These classes are used to associate state and propagate it throughout callbacks and promise chains. They allow storing data throughout the lifetime of a web request or any other asynchronous duration. It is similar to thread-local storage in other languages.
Basically, the nestjs-cls middleware wraps the asynchronous request, giving each request its own unique state or store. When the middleware initializes (in the bootstrap function), the tenant_id is pulled off the request and inserted in the store (The complete example repo adds the un-encrypted Iron-Session session to the store, which contains the tenant_id). This store can then be referenced (using the ClsService provider) anywhere in the app, without making the given provider it's injected into request-scoped (which includes the aforementioned Custom Factory Provider that creates the extended Prisma client)
More simply, AsyncLocalStorage lets us initialize and reference state that is scoped to each request without affecting the injection scope of providers which reference that state, which is useful because the Request Injection Scope can negatively impact performance.
End result: we can do multi-tenant stuff in NestJS with Prisma without needing to instantiate multiple PrismaClient instances (or create multiple database connections), which means this solution is scalable, as it doesn't incur the performance penalties which come with request-scoped providers, and there's no risk of hitting the PostgreSQL unique connection cap, which defaults to 100.
You can verify that this is the case with the example repo by looking at the logs for the backend service and localhost/nest/stats, after logging in and out as different users.
Thanks man for the detailed explanation. Since prisma client extensions are still in preview stage, I went with using prisma middleware to add the tenant filter. Still WIP. Will add a comment in here when fully completed
If you're going the middleware route, I strongly recommend load testing your application in a way that simulates multiple tenants sending requests at the same time. In my personal experience (after working through several multi-tenant solutions), certain approaches will seemingly work fine during development, but will have obvious issues in a real-world (or approximate) scenario.
For doing the tests, I like to use browser automation (with Playwright), primarily because it tests the auth flow in full. Typically I'll set up a GET endpoint that's only available when NODE_ENV=test that dumps the data I want to check (to make sure tenancy is working correctly, etc).
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
useFactory is employed because we want to inject the extended prisma client returned by calling
prisma.$extend(...), and you need useFactory for the sort of dynamic provider that's necessary to do this.More importantly, creating the extended prisma client with a Custom Factory Provider means the NestJS Dependency Injection system can be utilized when creating the extended client.
This does not create mutliple prisma clients (
new PrismaClient(...)) or multiple database connections per tenant or per request, as extended clients share the main client's connection. From the Prisma docs,If the extended client provider is NOT request-scoped it will should only create a single extended client that will be used for the lifetime of the server process. If the useFactory provider becomes request-scoped in your particular application (somehow), the useFactory function will run on every request. However, this isn't really a problem (extended prisma clients are meant to be disposable).
This single extended prisma client works for all tenants through the use of AsyncLocalStorage, which is facilitated by nestjs-cls, to create a unique context for each request. From the Node docs:
Basically, the nestjs-cls middleware wraps the asynchronous request, giving each request its own unique state or store. When the middleware initializes (in the bootstrap function), the tenant_id is pulled off the request and inserted in the store (The complete example repo adds the un-encrypted Iron-Session session to the store, which contains the tenant_id). This store can then be referenced (using the
ClsServiceprovider) anywhere in the app, without making the given provider it's injected into request-scoped (which includes the aforementioned Custom Factory Provider that creates the extended Prisma client)More simply, AsyncLocalStorage lets us initialize and reference state that is scoped to each request without affecting the injection scope of providers which reference that state, which is useful because the Request Injection Scope can negatively impact performance.
End result: we can do multi-tenant stuff in NestJS with Prisma without needing to instantiate multiple PrismaClient instances (or create multiple database connections), which means this solution is scalable, as it doesn't incur the performance penalties which come with request-scoped providers, and there's no risk of hitting the PostgreSQL unique connection cap, which defaults to 100.
You can verify that this is the case with the example repo by looking at the logs for the
backendservice and localhost/nest/stats, after logging in and out as different users.Other docs:
Nestjs Factory Providers: docs.nestjs.com/fundamentals/custo...
Prisma Client extensions: prisma.io/docs/concepts/components...
Thanks man for the detailed explanation. Since prisma client extensions are still in preview stage, I went with using prisma middleware to add the tenant filter. Still WIP. Will add a comment in here when fully completed
You're welcome, good luck!
If you're going the middleware route, I strongly recommend load testing your application in a way that simulates multiple tenants sending requests at the same time. In my personal experience (after working through several multi-tenant solutions), certain approaches will seemingly work fine during development, but will have obvious issues in a real-world (or approximate) scenario.
For doing the tests, I like to use browser automation (with Playwright), primarily because it tests the auth flow in full. Typically I'll set up a GET endpoint that's only available when NODE_ENV=test that dumps the data I want to check (to make sure tenancy is working correctly, etc).