js

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

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL RLS. Master tenant isolation, custom guards, and security best practices.

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

I’ve been building software for over a decade, and nothing has challenged me more than creating secure, scalable multi-tenant applications. Recently, a client experienced a data leakage incident that could have been prevented with proper tenant isolation. That moment cemented my belief that every SaaS developer needs to master multi-tenancy fundamentals. Today, I want to share my practical approach using NestJS, Prisma, and PostgreSQL’s Row-Level Security.

Multi-tenancy isn’t just an architectural pattern—it’s a commitment to data integrity. When you’re serving multiple customers from a single application, each tenant’s data must remain completely isolated. Have you ever considered what happens when a simple query accidentally returns another company’s information? The consequences can be devastating for both your customers and your business reputation.

Let me show you how I structure these applications. We’ll use a shared database with Row-Level Security because it offers the best balance between cost efficiency and security. The key insight? PostgreSQL’s RLS policies act as an invisible firewall around each tenant’s data.

Here’s how I set up the database foundation:

CREATE TABLE tenants (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    subdomain VARCHAR(100) UNIQUE NOT NULL
);

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    email VARCHAR(255) NOT NULL,
    UNIQUE(tenant_id, email)
);

ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY users_tenant_policy ON users
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID);

Notice how every table includes a tenant_id column? This becomes the anchor for all our security policies. The magic happens when we set the current tenant context before executing any query.

Now, let’s integrate this with Prisma. I’ve found that extending the Prisma client to handle tenant context automatically saves countless hours of debugging:

// prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }

  async setTenantContext(tenantId: string) {
    await this.$executeRaw`SELECT set_config('app.current_tenant_id', ${tenantId}, true)`;
  }
}

But how do we determine which tenant we’re working with? In NestJS, I create a tenant interceptor that extracts this information from the JWT token or request headers:

// tenant.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { PrismaService } from './prisma.service';

@Injectable()
export class TenantInterceptor implements NestInterceptor {
  constructor(private prisma: PrismaService) {}

  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const tenantId = request.user?.tenantId;
    
    if (tenantId) {
      await this.prisma.setTenantContext(tenantId);
    }

    return next.handle();
  }
}

What happens when you need to ensure certain endpoints always have tenant context? That’s where custom guards come in handy:

// tenant.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class TenantGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return !!request.user?.tenantId;
  }
}

I use these guards alongside custom decorators to make the code more expressive:

// tenant.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const Tenant = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user.tenantId;
  },
);

Then in my controllers, the setup becomes beautifully simple:

@Controller('organizations')
@UseGuards(TenantGuard)
@UseInterceptors(TenantInterceptor)
export class OrganizationsController {
  constructor(private organizationsService: OrganizationsService) {}

  @Get()
  async findAll(@Tenant() tenantId: string) {
    return this.organizationsService.findAll(tenantId);
  }
}

Have you ever wondered how to handle database migrations in this environment? The strategy I use involves creating tenant-aware migration scripts that respect the isolation boundaries. For example, when adding new columns, I ensure they don’t leak information between tenants.

Testing becomes crucial in multi-tenant environments. I always create tests that simulate cross-tenant access attempts. What would happen if a user from Tenant A tried to access Tenant B’s data? Our security layers should catch this every single time.

Performance optimization requires special attention too. I implement connection pooling with tenant context preservation and carefully index tenant-specific queries. Remember that every additional WHERE clause for tenant_id needs proper indexing.

Security considerations extend beyond the database layer. I validate tenant permissions at every API endpoint, implement rate limiting per tenant, and regularly audit our RLS policies. How often do you review your security policies for potential gaps?

The beauty of this approach is its scalability. As your SaaS grows, you can transition to separate databases for larger tenants without rewriting your entire application. The abstraction layers we’ve built make such evolution possible.

Building multi-tenant applications has taught me that security isn’t a feature—it’s the foundation. Every design decision, from database schemas to API endpoints, must prioritize tenant isolation. The patterns I’ve shared here have served me well across multiple production applications, but I’m always learning from the community.

What challenges have you faced with multi-tenancy? I’d love to hear about your experiences and solutions. If this guide helped clarify multi-tenant architecture, please share it with other developers who might benefit. Your comments and questions help all of us build better, more secure applications.

Keywords: multi-tenant saas nestjs, nestjs prisma multi-tenancy, row-level security postgresql, nestjs tenant isolation, saas application architecture, multi-tenant database design, nestjs custom guards decorators, prisma row level security, postgresql multi-tenancy patterns, nestjs saas development



Similar Posts
Blog Image
Build Complete E-Commerce Order Management System: NestJS, Prisma, Redis Queue Processing Tutorial

Learn to build a complete e-commerce order management system using NestJS, Prisma, and Redis queue processing. Master scalable architecture, async handling, and production-ready APIs. Start building today!

Blog Image
Complete Guide to Integrating Prisma with GraphQL: Build Type-Safe APIs with Modern Database Toolkit

Learn how to integrate Prisma with GraphQL for type-safe APIs, seamless database operations, and improved developer productivity. Master modern API development today.

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

Learn to build a real-time collaborative document editor using Socket.io, Operational Transforms & React. Master conflict resolution, user presence & scaling.

Blog Image
Build TypeScript Event Sourcing Systems with EventStore and Express - Complete Developer Guide

Learn to build resilient TypeScript systems with Event Sourcing, EventStoreDB & Express. Master CQRS, event streams, snapshots & microservices architecture.

Blog Image
Complete Guide to Next.js with Prisma ORM Integration: Type-Safe Full-Stack Development in 2024

Learn to integrate Next.js with Prisma ORM for type-safe database operations. Build full-stack apps with seamless schema management and optimized performance.

Blog Image
Complete Guide to Building Real-Time Apps with Svelte and Supabase Integration

Learn how to integrate Svelte with Supabase for rapid web development. Build real-time apps with PostgreSQL, authentication, and reactive UI components seamlessly.