js

Build Multi-Tenant SaaS with NestJS, Prisma, PostgreSQL: Complete RLS Implementation Guide

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL RLS. Master tenant isolation, authentication & performance optimization.

Build Multi-Tenant SaaS with NestJS, Prisma, PostgreSQL: Complete RLS Implementation Guide

Building robust multi-tenant SaaS applications has become increasingly vital in my work with cloud-native systems. Just last month, a client’s data isolation requirements forced me to reevaluate traditional approaches, leading me to PostgreSQL’s Row-Level Security combined with NestJS and Prisma. This combination creates a scalable foundation that maintains strict tenant separation while simplifying infrastructure. Let me walk you through how this works.

Multi-tenancy means serving multiple customers from a single application instance. Why choose shared-database RLS over alternatives? When you have hundreds of tenants, managing separate databases becomes impractical. RLS provides data isolation at the database level while keeping operational complexity low. How does this actually prevent tenant data leaks? Let’s examine the architecture.

Start with a clean NestJS setup:

nest new saas-app --strict
npm install @prisma/client prisma
npx prisma init

Our Prisma schema defines tenant-aware models. Notice the tenantId field in every table:

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

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

The real magic happens in PostgreSQL. We enable Row-Level Security and create isolation policies:

ALTER TABLE "User" ENABLE ROW LEVEL SECURITY;

CREATE POLICY user_tenant_isolation ON "User"
FOR ALL USING (
  "tenantId" = current_setting('app.current_tenant')::text
);

This ensures every query automatically filters by tenant context. But how do we set that context securely?

Authentication ties users to tenants. We implement a JWT strategy containing tenant IDs:

// auth.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  validate(payload: { sub: string; tenantId: string }) {
    return { userId: payload.sub, tenantId: payload.tenantId };
  }
}

Before executing queries, we set the tenant context using Prisma middleware:

// prisma.service.ts
this.prisma.$use(async (params, next) => {
  const tenantId = getCurrentTenantId();
  
  if (params.model === 'User') {
    params.args.where = { ...params.args.where, tenantId };
  }

  return next(params);
});

For RLS to work, we must set the PostgreSQL session variable before each operation:

// tenant.decorator.ts
export const SetTenantContext = createParamDecorator(
  (_, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const tenantId = request.user?.tenantId;
    
    return this.prisma.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, false)`;
  }
);

Now consider performance. Without proper indexing, tenant queries become slow as data grows. Add composite indexes:

model Task {
  tenantId String
  projectId String

  @@index([tenantId, projectId])
}

For testing, we mock tenant contexts:

// test.setup.ts
beforeEach(() => {
  jest.spyOn(tenantService, 'getTenantId').mockReturnValue('test_tenant');
});

During deployment, remember these key steps:

  1. Enable connection pooling with PgBouncer
  2. Set pool_timeout to recycle connections
  3. Use SET ROLE for RLS privileges
  4. Enable SSL for all database connections

Common pitfalls? Forgetting to add tenantId in every table relation tops the list. Another is neglecting to test RLS policies with direct database access. Always verify isolation by switching tenant contexts manually.

The beauty of this approach shines when onboarding new tenants:

// tenant.service.ts
async createTenant(subdomain: string) {
  return this.prisma.$transaction([
    prisma.tenant.create({ data: { subdomain } }),
    prisma.$executeRaw`CREATE SCHEMA IF NOT EXISTS ${subdomain}`
  ]);
}

Notice we’re using schemas for supplementary data? This hybrid approach combines RLS efficiency with schema flexibility for tenant-specific customizations.

Building SaaS applications requires thoughtful tradeoffs. By leveraging PostgreSQL’s RLS, we achieve robust data isolation without sacrificing maintainability. Prisma’s middleware keeps our code clean, while NestJS provides the architectural backbone. What other patterns have you found effective for multi-tenancy?

I’ve shared the core techniques that solved real-world isolation challenges for my clients. If this approach resonates with your projects, let me know your thoughts in the comments. Found this useful? Share it with your team facing similar SaaS architecture decisions.

Keywords: multi-tenant SaaS NestJS, NestJS Prisma PostgreSQL tutorial, PostgreSQL row-level security RLS, multi-tenancy database design patterns, tenant isolation SaaS architecture, NestJS authentication authorization guide, Prisma multi-tenant configuration, scalable SaaS application development, PostgreSQL RLS implementation tutorial, tenant-aware API development



Similar Posts
Blog Image
Complete Next.js Prisma Integration Guide: Build Type-Safe Full-Stack Applications in 2024

Learn how to integrate Next.js with Prisma for powerful full-stack applications. Build type-safe, database-driven apps with seamless API routes and improved productivity.

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

Learn how to integrate Next.js with Prisma ORM for type-safe, full-stack applications. Complete guide with setup, API routes, and best practices.

Blog Image
Build Event-Driven Microservices with NestJS, Redis Streams, and TypeScript: Complete Tutorial

Learn to build scalable event-driven microservices with NestJS, Redis Streams & TypeScript. Complete guide with code examples, error handling & testing strategies.

Blog Image
Building High-Performance Event-Driven Microservices with NestJS, Apache Kafka, and Redis

Learn to build scalable event-driven microservices using NestJS, Apache Kafka, and Redis. Master event choreography, saga patterns, error handling, and performance optimization for high-throughput systems.

Blog Image
Next.js Prisma Integration Guide: Build Type-Safe Full-Stack Apps with Modern Database ORM

Learn how to integrate Next.js with Prisma ORM for type-safe, database-driven web apps. Build faster with seamless full-stack development and modern tooling.

Blog Image
Build Type-Safe Full-Stack Apps: Complete Next.js and Prisma Integration Guide for Modern Developers

Learn how to integrate Next.js with Prisma for type-safe full-stack development. Build robust applications with auto-generated TypeScript types and seamless database operations.