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
Build High-Performance GraphQL APIs: NestJS, Prisma & DataLoader Pattern Guide

Learn to build scalable GraphQL APIs using NestJS, Prisma, and DataLoader. Optimize performance, solve N+1 queries, implement auth, and deploy production-ready APIs.

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

Learn how to integrate Next.js with Prisma ORM for powerful full-stack development. Build type-safe applications with seamless database management and API routes.

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

Learn how to integrate Next.js with Prisma ORM for type-safe, scalable web applications. Complete guide to setup, migrations & best practices.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Database Operations

Learn how to integrate Next.js with Prisma ORM for type-safe database operations. Build scalable full-stack apps with seamless TypeScript support.

Blog Image
Build Type-Safe Event-Driven Microservices with NestJS, RabbitMQ, and Prisma Complete Guide

Learn to build scalable type-safe event-driven microservices with NestJS, RabbitMQ & Prisma. Complete guide with SAGA patterns, testing & deployment tips.

Blog Image
Master Event Sourcing with EventStore and Node.js: Complete Implementation Guide with CQRS Patterns

Master Event Sourcing with EventStoreDB and Node.js. Learn CQRS, aggregates, projections, and testing. Complete implementation guide with best practices.