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: Building Multi-Tenant SaaS with NestJS, Prisma, and PostgreSQL Row-Level Security

Build secure multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL RLS. Learn tenant isolation, scalable architecture & performance optimization.

Blog Image
Build a Real-Time Collaborative Document Editor: Socket.io, Operational Transforms, and Redis Tutorial

Learn to build a real-time collaborative document editor using Socket.io, Operational Transforms & Redis. Complete guide with conflict resolution and scaling.

Blog Image
Building a Distributed Rate Limiting System with Redis and Node.js: Complete Implementation Guide

Learn to build scalable distributed rate limiting with Redis and Node.js. Implement Token Bucket, Sliding Window algorithms, Express middleware, and production deployment strategies.

Blog Image
Build Event-Driven Architecture: NestJS, Redis Streams & TypeScript Complete Tutorial

Learn to build scalable event-driven architecture with NestJS, Redis Streams & TypeScript. Master microservices communication, consumer groups & monitoring.

Blog Image
Build Multi-Tenant SaaS with NestJS, Prisma, and PostgreSQL Row-Level Security Tutorial

Learn to build secure multi-tenant SaaS apps with NestJS, Prisma, and PostgreSQL RLS. Master tenant isolation, JWT auth, and scalable architecture patterns.

Blog Image
How to Build Scalable Event-Driven Microservices with NestJS, RabbitMQ, and Redis: Complete Guide

Learn to build scalable event-driven microservices with NestJS, RabbitMQ, and Redis. Master message queuing, caching, CQRS patterns, and production deployment strategies.