js

Complete Multi-Tenant SaaS Guide: NestJS, Prisma, Row-Level Security Implementation

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

Complete Multi-Tenant SaaS Guide: NestJS, Prisma, Row-Level Security Implementation

I’ve been thinking a lot about building software that serves multiple customers securely and efficiently. After working on several SaaS products, I’ve seen how critical it is to get the foundation right from day one. Today, I want to share my approach to creating multi-tenant applications using NestJS, Prisma, and PostgreSQL’s powerful security features. This combination has helped me build systems that scale while keeping customer data completely isolated.

What if you could serve hundreds of customers from the same application while ensuring none of them ever sees each other’s data?

Let me show you how I approach multi-tenancy. The key decision comes down to data isolation strategy. I prefer the shared database with row-level security approach because it balances security, performance, and maintenance overhead beautifully. You get the cost benefits of shared infrastructure without compromising on data separation.

Here’s how I set up the database schema using Prisma:

model Tenant {
  id        String   @id @default(cuid())
  name      String
  slug      String   @unique
  isActive  Boolean  @default(true)
  users     User[]
  projects  Project[]
}

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

The real magic happens with PostgreSQL’s row-level security. This feature lets you create policies that automatically filter data based on the current tenant. Here’s how I implement it:

ALTER TABLE users ENABLE ROW LEVEL SECURITY;

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

Now, every query against the users table will only return rows where the tenant_id matches what we set in the session. But how do we ensure this tenant context is always available?

I handle this through middleware in my NestJS application. The middleware extracts tenant information from the JWT token or subdomain and sets it in the database session:

@Injectable()
export class TenantMiddleware implements NestMiddleware {
  constructor(private prisma: PrismaService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const tenantId = this.extractTenantId(req);
    
    await this.prisma.$executeRaw`
      SELECT set_config('app.current_tenant', ${tenantId}, true)
    `;
    
    next();
  }
}

Have you ever wondered how authentication works in a multi-tenant environment? I need to ensure users can only access their own tenant’s data, even during login.

Here’s my approach to tenant-aware authentication:

@Injectable()
export class AuthService {
  async validateUser(email: string, password: string, tenantId: string) {
    const user = await this.prisma.user.findFirst({
      where: { 
        email,
        tenantId,
        isActive: true 
      }
    });

    if (user && await bcrypt.compare(password, user.password)) {
      return user;
    }
    return null;
  }
}

When building services, I always make them tenant-aware. This means they automatically respect the tenant context without requiring manual filtering:

@Injectable()
export class ProjectService {
  async createProject(data: CreateProjectDto) {
    // Tenant context is automatically applied through RLS
    return this.prisma.project.create({
      data: {
        ...data,
        // No need to manually set tenantId - handled by RLS policies
      }
    });
  }

  async getUserProjects(userId: string) {
    // Only returns projects for the current tenant
    return this.prisma.project.findMany({
      where: { ownerId: userId }
    });
  }
}

What happens when you need to run background jobs or server-to-server communication that doesn’t have a natural tenant context?

I solve this by creating a TenantContext service that can manually set and clear tenant contexts:

@Injectable()
export class TenantContext {
  constructor(private prisma: PrismaService) {}

  async runForTenant(tenantId: string, operation: () => Promise<any>) {
    await this.prisma.$executeRaw`
      SELECT set_config('app.current_tenant', ${tenantId}, true)
    `;
    
    try {
      return await operation();
    } finally {
      await this.prisma.$executeRaw`
        SELECT set_config('app.current_tenant', '', true)
      `;
    }
  }
}

Testing becomes crucial in multi-tenant applications. I make sure to verify that data isolation works correctly:

describe('Multi-tenant Isolation', () => {
  it('should not leak data between tenants', async () => {
    // Create users in different tenants
    await createUserInTenant('tenant-1', 'user1@test.com');
    await createUserInTenant('tenant-2', 'user2@test.com');

    // Switch to tenant-1 context
    await setTenantContext('tenant-1');
    
    const users = await userService.findAll();
    expect(users).toHaveLength(1);
    expect(users[0].email).toBe('user1@test.com');
  });
});

Performance optimization is another area I focus on. Since all tenants share the same database, I make sure to add proper indexes:

model User {
  id       String @id @default(cuid())
  email    String @unique
  tenantId String
  
  @@index([tenantId])  // Critical for performance
  @@index([tenantId, email])  // Composite index for common queries
}

One challenge I often face is handling tenant-specific configurations. Some customers might need custom fields or different business rules. I handle this through a flexible JSON field in the tenant table:

interface TenantConfig {
  customFields?: Record<string, any>;
  businessRules?: BusinessRules;
  uiPreferences?: UIPreferences;
}

Building multi-tenant applications requires careful planning, but the payoff is enormous. You can serve multiple customers from a single codebase while maintaining strong data isolation. The combination of NestJS for application structure, Prisma for database access, and PostgreSQL for security gives you a robust foundation.

What questions do you have about implementing multi-tenancy in your own projects? Have you encountered any challenges with data isolation that I haven’t covered here?

I’d love to hear about your experiences building multi-tenant applications. If you found this guide helpful, please share it with your team or colleagues who might benefit from these patterns. Your comments and questions help me create better content, so don’t hesitate to reach out with your thoughts!

Keywords: multi-tenant SaaS application, NestJS multi-tenancy, Prisma row-level security, PostgreSQL RLS, tenant isolation authentication, scalable SaaS architecture, NestJS Prisma integration, multi-tenant database design, SaaS security patterns, tenant-aware applications



Similar Posts
Blog Image
Build a High-Performance Node.js File Upload Service with Streams, Multer, and AWS S3

Learn to build a scalable Node.js file upload service with streams, Multer & AWS S3. Includes progress tracking, resumable uploads, and production-ready optimization tips.

Blog Image
Prisma GraphQL Integration: Build Type-Safe APIs with Modern Database Operations and Full-Stack TypeScript Support

Learn how to integrate Prisma with GraphQL for end-to-end type-safe database operations. Build efficient, error-free APIs with TypeScript support.

Blog Image
Complete Guide to Next.js and Prisma Integration for Modern Full-Stack Development

Learn how to integrate Next.js with Prisma for powerful full-stack development. Get type-safe database access, seamless API routes, and rapid prototyping. Build modern web apps faster today!

Blog Image
Build Multi-Tenant SaaS with NestJS, Prisma, PostgreSQL: Complete Row-Level Security Guide

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL RLS. Master tenant isolation, authentication, and security best practices for production-ready applications.

Blog Image
Building Full-Stack TypeScript Apps: Complete Next.js and Prisma Integration Guide for Type-Safe Development

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

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM: Build Type-Safe Full-Stack Applications in 2024

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