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', '[email protected]');
    await createUserInTenant('tenant-2', '[email protected]');

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

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
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: Next.js Prisma Integration for Type-Safe Full-Stack Database Management in 2024

Learn how to integrate Next.js with Prisma for seamless full-stack database management. Build type-safe React apps with modern ORM capabilities and streamlined workflows.

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

Learn how to integrate Next.js with Prisma ORM for type-safe database operations, seamless schema management, and powerful full-stack development.

Blog Image
How to Build Production-Ready GraphQL APIs with NestJS, Prisma, and Redis Cache in 2024

Learn to build production-ready GraphQL APIs using NestJS, Prisma, and Redis cache. Master authentication, subscriptions, performance optimization, and testing strategies.

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
Complete Guide to 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 development. Build scalable web apps with robust database management and SSR.