js

Build Multi-Tenant SaaS Applications with NestJS, Prisma, and PostgreSQL Row-Level Security

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma, and PostgreSQL RLS. Complete guide with secure tenant isolation and database-level security. Start building today!

Build Multi-Tenant SaaS Applications with NestJS, Prisma, and PostgreSQL Row-Level Security

Building a Multi-Tenant SaaS Application with NestJS, Prisma, and PostgreSQL Row-Level Security

Lately, I’ve noticed many developers struggle when scaling applications to serve multiple clients securely. Just last month, a startup founder shared how their data leakage incident cost them a major client. This sparked my interest in documenting a robust approach to multi-tenancy. If you’re building SaaS products, this guide could save you from similar pitfalls.

Multi-Tenancy Architecture Overview
Multi-tenancy lets one application serve multiple customers while keeping their data separate. We use PostgreSQL’s Row-Level Security (RLS) to enforce isolation at the database layer. Why rely on database-level security? Because application bugs won’t compromise tenant data. This approach balances security with operational efficiency – all tenants share a single database schema while RLS acts as an enforced boundary. Ever wonder what prevents accidental data leaks between tenants? That’s RLS in action.

Project Setup and Dependencies
Start a new NestJS project and install key packages:

nest new saas-app
npm install @prisma/client prisma @nestjs/jwt bcryptjs
npx prisma init

Our structure organizes code by domain:

src/
├─ tenants/
├─ users/
├─ prisma/
│  └─ schema.prisma
└─ auth/

This keeps tenant logic centralized while allowing module expansion.

Database Schema Design with RLS
Define models with tenant relationships in Prisma:

model Tenant {
  id   String @id @default(cuid())
  name String
  users User[]
}

model User {
  id       String @id @default(cuid())
  email    String @unique
  tenant   Tenant @relation(fields: [tenantId], references: [id])
  tenantId String
}

Critical RLS setup for the users table:

CREATE POLICY tenant_isolation_policy ON users
USING (tenant_id = current_setting('app.tenant_id')::UUID);

This policy ensures users only see rows matching their tenant ID. Notice how we use PostgreSQL’s session variables for context? That’s our gateway to automatic isolation.

Configuring Prisma for Multi-Tenancy
Extend PrismaClient to handle tenant context:

// prisma.service.ts
@Injectable()
export class PrismaService extends PrismaClient {
  async setTenant(tenantId: string) {
    await this.$executeRaw`SELECT set_config('app.tenant_id', ${tenantId}, true)`;
  }
}

Before executing any query, call setTenant(). This injects the tenant context into PostgreSQL’s session. What happens if we forget this step? RLS blocks all data access – a safe default behavior.

Building Tenant-Aware NestJS Services
Create a tenant context service using NestJS’s dependency injection:

// tenant-context.service.ts
@Injectable()
export class TenantContext {
  private tenantId: string;

  setTenantId(id: string) {
    this.tenantId = id;
  }

  getTenantId() {
    return this.tenantId;
  }
}

Inject this into services:

// users.service.ts
@Injectable()
export class UsersService {
  constructor(
    private prisma: PrismaService,
    private tenantContext: TenantContext
  ) {}

  async getUsers() {
    await this.prisma.setTenant(this.tenantContext.getTenantId());
    return this.prisma.user.findMany();
  }
}

This pattern keeps tenant logic DRY and consistent.

Implementing Tenant Context Guards
Use guards to auto-set tenant context from requests:

// tenant.guard.ts
@Injectable()
export class TenantGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const tenantId = request.headers['x-tenant-id'];
    
    if (!tenantId) throw new UnauthorizedException();
    
    const tenantContext = context
      .switchToHttp()
      .getRequest()
      .app.get(TenantContext);
    
    tenantContext.setTenantId(tenantId);
    return true;
  }
}

Apply globally in your main module:

// app.module.ts
@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: TenantGuard,
    },
  ],
})

Now every request automatically gets tenant-aware services. How much boilerplate does this eliminate? All of it.

Creating Multi-Tenant Controllers
Controllers remain clean thanks to underlying services:

@Controller('users')
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Get()
  async findAll() {
    return this.usersService.getUsers();
  }
}

The controller needs no tenant logic – it’s all handled upstream.

Database Migrations and Seeding
Apply RLS policies via Prisma migrations:

npx prisma migrate dev --name enable_rls

Seed tenants with:

// prisma/seed.ts
await prisma.tenant.create({
  data: {
    name: 'Acme Inc',
    users: { create: [{ email: '[email protected]' }] },
  },
});

Testing Multi-Tenant Applications
Verify isolation with integration tests:

it('prevents cross-tenant data access', async () => {
  await setTenant('tenant_A');
  await createTestUser();
  
  await setTenant('tenant_B');
  const users = await getUsers();
  
  expect(users).toHaveLength(0);
});

This test proves our RLS policies work as intended.

Performance Optimization Strategies

  1. Connection Pooling: Use PgBouncer to manage PostgreSQL connections
  2. Indexing: Add composite indexes on (tenant_id, created_at)
  3. Caching: Redis cache per-tenant queries with TTL
  4. Read Replicas: Route reads to replicas using prisma.$extends

Common Pitfalls and Troubleshooting

  • Missing RLS Policy: Forgetting to enable RLS on new tables
  • Context Leaks: Not resetting tenant context between requests
  • Migration Order: Applying RLS policies before creating tables
  • Type Casting: Mismatched UUID types in session variables

If queries return empty unexpectedly, check:

  1. Tenant ID header presence
  2. RLS policy activation status
  3. Session variable type matches column type

Alternative Approaches
While RLS offers strong security, consider these when needed:

  • Separate Schemas: CREATE SCHEMA tenant_xyz for extreme isolation
  • Database-per-Tenant: For large enterprises with compliance needs
  • Application-Level Filtering: Where RLS isn’t available (not recommended)

RLS provides the best balance for most SaaS applications between security and operational simplicity.


Building multi-tenant applications requires thoughtful design, but the payoff is immense. With PostgreSQL RLS and NestJS, you get enterprise-grade isolation without complex infrastructure. I’ve used this approach in production for 3+ years with zero data leaks. What questions do you have about your implementation?

If this guide helped you, share it with your team or colleagues building SaaS products. Have you tried different multi-tenancy strategies? Share your experiences in the comments below – let’s learn from each other.

Keywords: multi-tenant SaaS application, NestJS multi-tenancy, PostgreSQL row-level security, Prisma ORM multi-tenant, SaaS architecture NestJS, tenant isolation database, NestJS Prisma PostgreSQL, multi-tenant database design, SaaS development TypeScript, tenant-aware application security



Similar Posts
Blog Image
Complete Guide to Building Full-Stack Web Applications with Next.js and Prisma Integration

Learn how to integrate Next.js with Prisma ORM for powerful full-stack web apps. Build type-safe, performant applications with seamless database operations.

Blog Image
Build Real-Time Collaborative Document Editor with Socket.io Redis and Operational Transforms Complete Guide

Build a high-performance collaborative document editor with Socket.io, Redis & Operational Transforms. Learn real-time editing, conflict resolution & scalable WebSocket architecture for concurrent users.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Development

Learn how to integrate Next.js with Prisma ORM for type-safe, full-stack web applications. Build faster with end-to-end TypeScript support and seamless data flow.

Blog Image
Build Production-Ready GraphQL APIs: NestJS, Prisma, and Redis Caching Complete Guide

Build production-ready GraphQL APIs with NestJS, Prisma & Redis caching. Learn authentication, performance optimization & deployment best practices.

Blog Image
Build Real-time Web Apps: Complete Svelte and Supabase Integration Guide for Modern Developers

Learn to integrate Svelte with Supabase for building high-performance real-time web applications. Discover seamless data sync, authentication, and reactive UI updates.

Blog Image
Building Production-Ready Event-Driven Microservices with NestJS, RabbitMQ, and MongoDB

Build production-ready event-driven microservices with NestJS, RabbitMQ & MongoDB. Learn Saga patterns, error handling & deployment strategies.