Lately, I’ve been thinking about efficient ways to build SaaS applications that serve multiple customers securely. Why? Because traditional approaches often lead to complex infrastructure or data isolation challenges. That’s what brought me to explore multi-tenancy with NestJS, Prisma, and PostgreSQL’s Row-Level Security. This combination offers a balanced approach between security, scalability, and maintainability.
Multi-tenancy means a single application serves multiple customers while keeping their data strictly separated. There are several ways to achieve this, but the shared schema approach using PostgreSQL’s RLS stands out. Why? Because it maintains strong isolation while keeping costs manageable. Consider how a database-per-tenant model quickly becomes expensive as your user base grows. Or how schema-per-tenant adds operational complexity. The shared schema approach? It gives us high isolation with excellent scalability.
// Pattern comparison
type SharedSchemaRLS = {
isolation: 'High';
complexity: 'Medium';
cost: 'Low';
scalability: 'Excellent';
};
Setting up the project requires careful structure. I organize code into clear domains: authentication, tenant management, database operations, and business modules. This separation keeps responsibilities clean. How do we start? First, install NestJS and essential dependencies like Prisma, Passport, and configuration tools. Environment configuration is crucial too – I use a dedicated file to manage database settings, including RLS activation flags.
npm install @nestjs/passport prisma @prisma/client
npx prisma init
Now, PostgreSQL RLS configuration is where the magic happens. We enable RLS on tenant-scoped tables and create policies that reference the current tenant context. Notice how every policy checks against app.current_tenant_id
? This session variable becomes our isolation gatekeeper.
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.current_tenant_id')::text);
For the Prisma integration, I extend the client to handle tenant context per request. The custom service executes a raw query to set PostgreSQL’s session variable, activating RLS filters automatically. What happens if we forget this step? All tenant data becomes visible – a critical security gap.
@Injectable({ scope: Scope.REQUEST })
export class PrismaTenantService extends PrismaClient {
async setTenantContext(tenantId: string) {
await this.$executeRaw`SELECT set_tenant_id(${tenantId})`;
}
}
Tenant resolution happens through middleware. I extract the tenant identifier from subdomains or JWT tokens, then attach it to the request. This happens before route handlers execute, ensuring downstream services operate within the correct tenant scope. How might a malicious actor try to bypass this? They could manipulate request headers – that’s why we validate tenant IDs against the database.
Authentication uses JWT strategies with tenant context baked into tokens. When a user logs in, we include their tenant ID in the payload. Guards then verify both token validity and tenant membership. Authorization goes further, checking user roles within their tenant context. Can a “viewer” role edit sensitive data? Absolutely not – our role-based access control prevents it.
@Injectable()
export class TenantGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
return isValidTenant(request.user.tenantId);
}
}
Database migrations need special attention. We apply global schema changes with Prisma Migrate, but tenant-specific data seeding happens through custom scripts. These scripts run within the RLS context, ensuring data belongs to the correct tenant. Performance-wise, connection pooling and query optimization are non-negotiable. I use PgBouncer for connection management and avoid N+1 queries through Prisma’s relation loading.
Testing is multi-layered: unit tests for services, integration tests for API endpoints, and security tests validating RLS enforcement. One critical test? Verify that users from Tenant A can’t access Tenant B’s data. What’s worse than a theoretical security flaw? An actual data leak in production.
Common pitfalls include forgetting RLS activation on new tables or misconfiguring connection pools. Always monitor query performance and tenant-specific metrics. Tools like Prometheus help track anomalies early.
This architecture scales well, but alternatives exist. Schema-per-tenant offers stronger isolation at higher cost. Database-per-tenant works for highly regulated industries. Choose what fits your constraints. Personally, I find the RLS approach delivers the most value for typical SaaS scenarios.
What challenges have you faced with multi-tenant systems? Building this changed how I approach SaaS development. If you found this useful, share it with your network. Have questions or insights? Let’s discuss in the comments.