As I built my latest SaaS product, a critical question emerged: how can I securely serve multiple customers from a single application without compromising their data? That’s when I decided to implement multi-tenancy using NestJS, Prisma, and PostgreSQL’s Row-Level Security. Let me share this journey with you.
First, what exactly is multi-tenancy? It’s an architecture where one application instance serves multiple customers (tenants), with strict data separation between them. Why choose this approach? It reduces costs, simplifies maintenance, and scales efficiently. But how do we ensure ironclad data isolation? That’s where PostgreSQL RLS comes in.
Let’s set up our project. Start with these commands:
nest new saas-app
cd saas-app
npm install @prisma/client prisma
npx prisma init
Now, imagine our database schema. We need tenants, users, and organizations. Here’s a simplified Prisma schema:
model Tenant {
id String @id @default(cuid())
name String
slug String @unique
}
model User {
id String @id @default(cuid())
email String @unique
password String
tenant Tenant @relation(fields: [tenantId], references: [id])
tenantId String
}
The real magic happens with PostgreSQL Row-Level Security. This feature restricts database access at the row level. Try this SQL policy:
CREATE POLICY tenant_isolation_policy ON users
FOR ALL USING (
tenant_id = current_setting('app.current_tenant_id')
);
See how it uses the tenant context? That’s our security backbone. Now, what happens when a user makes a request? We need to identify their tenant first.
In NestJS, we resolve tenants using middleware. Here’s a simplified version:
// tenant.middleware.ts
import { Request, Response, NextFunction } from 'express';
export function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
const tenantId = req.headers['x-tenant-id'] || req.hostname;
req.tenantId = tenantId;
next();
}
But how do we enforce this tenant context in database queries? We extend Prisma’s client:
// prisma.service.ts
import { PrismaClient } from '@prisma/client';
import { Injectable, OnModuleInit } from '@nestjs/common';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async withTenant(tenantId: string) {
return this.$extends({
query: {
async $allOperations({ args, query }) {
const [, result] = await this.$transaction([
this.$executeRaw`SELECT set_config('app.current_tenant_id', ${tenantId}, TRUE)`,
query(args)
]);
return result;
}
}
});
}
}
Notice how we set the tenant ID before each query? This activates our RLS policies automatically. What about authentication? We use JWT tokens containing the tenant ID.
When onboarding new tenants, our process must be seamless. Consider this user signup flow:
// auth.service.ts
async signUp(tenantSlug: string, signUpDto: SignUpDto) {
const tenant = await this.prisma.tenant.findUnique({
where: { slug: tenantSlug }
});
const hashedPassword = await bcrypt.hash(signUpDto.password, 10);
return this.prisma.user.create({
data: {
email: signUpDto.email,
password: hashedPassword,
tenantId: tenant.id
}
});
}
Testing is crucial. How do we verify tenant isolation? I use integration tests that:
- Create two test tenants
- Add data to both
- Verify neither can access the other’s data
Performance considerations? Always:
- Index tenant_id columns
- Monitor connection pooling
- Cache tenant-specific data carefully
- Rate limit per tenant
Common pitfalls I’ve encountered:
- Forgetting to set tenant context in background jobs
- Caching data without tenant segregation
- Not testing RLS policies thoroughly
- Missing indexes on tenant_id columns
What if you need stricter isolation? For enterprise customers, consider separate databases. But for most SaaS applications, RLS provides excellent security with simpler operations. I’ve found this combination of NestJS, Prisma, and PostgreSQL RLS delivers robust multi-tenancy without excessive complexity.
Building this architecture taught me valuable lessons about scalable security. Have you tried implementing RLS before? What challenges did you face? If you found this helpful, share it with your network! Let me know your thoughts in the comments - I’d love to hear about your SaaS architecture experiences.