js

Complete Multi-Tenant SaaS Guide: NestJS, Prisma, PostgreSQL Row-Level Security from Setup to Production

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL RLS. Master tenant isolation, security & architecture. Start building now!

Complete Multi-Tenant SaaS Guide: NestJS, Prisma, PostgreSQL Row-Level Security from Setup to Production

I’ve been thinking a lot lately about what separates a simple application from a truly robust SaaS platform. The complexity isn’t just in the features—it’s in how you handle multiple customers securely and efficiently. That’s why I want to walk you through building a multi-tenant system with NestJS, Prisma, and PostgreSQL’s row-level security. This combination creates a foundation that scales while keeping data completely isolated between customers.

Let me show you how to set this up properly.

First, we need to establish our database schema with tenant isolation built into its core. Every table that contains tenant-specific data requires a tenantId field. This becomes our anchor point for data separation.

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

But how do we ensure that users from one tenant can’t accidentally access another tenant’s data? That’s where PostgreSQL’s row-level security comes into play.

ALTER TABLE users ENABLE ROW LEVEL SECURITY;

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

Now, what happens when a request comes in? We need to identify which tenant it belongs to and set that context before any database operation. This is where middleware becomes crucial.

@Injectable()
export class TenantMiddleware implements NestMiddleware {
  constructor(private readonly configService: ConfigService) {}

  use(req: Request, res: Response, next: NextFunction) {
    const tenantId = this.extractTenantFromRequest(req);
    if (tenantId) {
      req['tenantId'] = tenantId;
    }
    next();
  }
}

Have you considered how this middleware integrates with your database operations? We need a Prisma client that’s aware of the current tenant context.

@Injectable()
export class TenantPrismaService {
  constructor(
    private readonly configService: ConfigService,
    private readonly request: Request
  ) {}

  getClient(): PrismaClient {
    const tenantId = this.request['tenantId'];
    const client = new PrismaClient();
    
    // Set the tenant context for this connection
    client.$executeRaw`SELECT set_config('app.current_tenant_id', ${tenantId}, true)`;
    
    return client;
  }
}

What about authentication and authorization? We need guards that understand multi-tenancy.

@Injectable()
export class TenantGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const userTenantId = request.user.tenantId;
    const requestTenantId = request.tenantId;

    return userTenantId === requestTenantId;
  }
}

Testing becomes particularly important in a multi-tenant environment. You need to verify that data isolation works as expected.

describe('Multi-tenant Data Isolation', () => {
  it('should not allow cross-tenant data access', async () => {
    const tenantAClient = await createTestClient('tenant-a');
    const tenantBClient = await createTestClient('tenant-b');

    // Create data in tenant A
    await tenantAClient.user.create({ data: { email: '[email protected]' } });

    // Try to access from tenant B
    const result = await tenantBClient.user.findMany();
    expect(result).toHaveLength(0);
  });
});

Performance considerations are different in multi-tenant systems. Database indexes need special attention.

model User {
  id       String @id @default(cuid())
  email    String
  tenantId String
  
  @@index([tenantId])
  @@index([email, tenantId])
}

What about database migrations? They need to handle both schema changes and RLS policies.

async function runMigrations() {
  // Schema migrations
  await prisma.$executeRaw`ALTER TABLE users ADD COLUMN IF NOT EXISTS new_column TEXT`;
  
  // RLS policy updates
  await prisma.$executeRaw`
    CREATE POLICY IF NOT EXISTS user_select_policy ON users
    FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id')::text)
  `;
}

Error handling requires special consideration too. You don’t want to leak tenant information in error messages.

@Catch(PrismaClientKnownRequestError)
export class PrismaClientExceptionFilter implements ExceptionFilter {
  catch(exception: PrismaClientKnownRequestError, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse();
    
    // Sanitize error messages to remove tenant-specific information
    const safeMessage = this.sanitizeErrorMessage(exception.message);
    
    response.status(500).json({
      error: 'Database operation failed',
      message: safeMessage
    });
  }
}

Building a multi-tenant application requires thinking about every layer of your stack. From database design to API endpoints, each component must respect tenant boundaries. The patterns I’ve shown here provide a solid foundation, but remember that every application has unique requirements.

What challenges have you faced with multi-tenancy? I’d love to hear about your experiences and solutions. If you found this helpful, please share it with others who might benefit from these patterns. Your comments and questions help make these guides better for everyone.

Keywords: multi-tenant SaaS NestJS, PostgreSQL row-level security, Prisma multi-tenancy, NestJS tenant isolation, SaaS application architecture, PostgreSQL RLS tutorial, multi-tenant database design, NestJS Prisma integration, tenant-aware middleware, SaaS security best practices



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

Learn to integrate Next.js with Prisma ORM for type-safe full-stack development. Build robust apps with seamless database management and TypeScript support.

Blog Image
Build Redis API Rate Limiting with Express: Token Bucket, Sliding Window Implementation Guide

Learn to build production-ready API rate limiting with Redis & Express. Covers Token Bucket, Sliding Window algorithms, distributed limiting & monitoring. Complete implementation guide.

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

Learn to build powerful full-stack apps by integrating Next.js with Prisma ORM for type-safe database operations. Boost productivity with seamless TypeScript support.

Blog Image
Complete Event-Driven Microservices with NestJS, RabbitMQ and MongoDB: Step-by-Step Guide 2024

Learn to build event-driven microservices with NestJS, RabbitMQ & MongoDB. Master distributed architecture, Saga patterns, and deployment strategies in this comprehensive guide.

Blog Image
Build High-Performance Event-Driven Microservices with Fastify, Redis Streams, and TypeScript

Learn to build high-performance event-driven microservices with Fastify, Redis Streams & TypeScript. Includes saga patterns, monitoring, and deployment strategies.

Blog Image
Build High-Performance Real-Time Analytics Pipeline with ClickHouse Node.js Streams Socket.io Tutorial

Build a high-performance real-time analytics pipeline with ClickHouse, Node.js Streams, and Socket.io. Master scalable data processing, WebSocket integration, and monitoring. Start building today!