js

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

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

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

I’ve been building SaaS applications for years, and one challenge consistently stands out: how to securely and efficiently serve multiple customers from a single application instance. That’s why I want to share my approach to creating a robust multi-tenant architecture using NestJS, Prisma, and PostgreSQL’s powerful Row Level Security feature.

What if you could build an application where each customer’s data remains completely isolated, yet you maintain a single codebase and database? This isn’t just theoretical—it’s achievable with the right architecture decisions.

Let’s start with the foundation: database design. Every table in our schema needs a tenant_id column. This simple addition becomes the cornerstone of our data isolation strategy. In Prisma, we define this relationship clearly, ensuring that every user, project, and subscription links back to a specific tenant.

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

But how do we ensure that users from one tenant can never access another tenant’s data? PostgreSQL’s Row Level Security provides the answer. We create policies that automatically filter queries based on the current tenant context.

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

Have you considered what happens when a user authenticates? Our JWT tokens must include the tenant context. When a user logs in, we verify their credentials and generate a token that includes their tenant identifier.

async function login(credentials: LoginDto) {
  const user = await prisma.user.findUnique({
    where: { email: credentials.email }
  });
  
  if (!user) throw new UnauthorizedException();
  
  const isValid = await compare(credentials.password, user.password);
  if (!isValid) throw new UnauthorizedException();
  
  return {
    access_token: this.jwtService.sign({
      sub: user.id,
      tenant: user.tenantId
    })
  };
}

In NestJS, we create a tenant-aware guard that extracts this information from the JWT and sets it in the request context. This becomes crucial for all subsequent database operations.

@Injectable()
export class TenantGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const tenantId = request.user?.tenant;
    
    if (!tenantId) {
      throw new ForbiddenException('Tenant context required');
    }
    
    // Set tenant for RLS
    request.tenantId = tenantId;
    return true;
  }
}

What about database connections? We use Prisma’s middleware to automatically set the tenant context for every query. This ensures that even if a developer forgets to include tenant filtering, the database itself enforces isolation.

prisma.$use(async (params, next) => {
  const tenantId = getCurrentTenantId(); // From request context
  
  if (params.model && tenantId) {
    if (params.action === 'findUnique' || params.action === 'findFirst') {
      params.args.where.tenantId = tenantId;
    }
    if (params.action === 'findMany') {
      params.args.where = { ...params.args.where, tenantId };
    }
  }
  
  return next(params);
});

Performance is always a concern in multi-tenant systems. We create composite indexes that include both the tenant_id and commonly queried fields. This ensures that queries remain fast even as our data grows across multiple tenants.

model User {
  id        String   @id @default(cuid())
  email     String
  tenantId  String
  // ... other fields
  
  @@index([tenantId, email])
  @@index([tenantId, createdAt])
}

Subscription management becomes straightforward with this architecture. Each tenant has a subscription record that determines their access level and features. We can easily enforce limits and track usage.

async function createProject(dto: CreateProjectDto) {
  const tenant = await getCurrentTenant();
  const subscription = await getTenantSubscription(tenant.id);
  
  if (subscription.plan === 'FREE' && await getProjectCount(tenant.id) >= 3) {
    throw new BadRequestException('Project limit reached');
  }
  
  return prisma.project.create({
    data: {
      ...dto,
      tenantId: tenant.id
    }
  });
}

Testing this architecture requires careful consideration. We create test utilities that can simulate multiple tenants simultaneously, ensuring our isolation holds under various scenarios.

describe('Multi-tenant isolation', () => {
  it('should prevent cross-tenant data access', async () => {
    const tenantA = await createTestTenant();
    const tenantB = await createTestTenant();
    
    // Create data for tenant A
    await setCurrentTenant(tenantA.id);
    const projectA = await createTestProject();
    
    // Try to access from tenant B context
    await setCurrentTenant(tenantB.id);
    const result = await getProject(projectA.id);
    
    expect(result).toBeNull();
  });
});

Deployment strategies vary based on scale. For most applications, starting with a single PostgreSQL instance with proper RLS policies provides excellent security while keeping costs manageable. As you grow, you might consider separate databases for your largest customers.

Monitoring becomes essential. We track query performance, tenant growth patterns, and subscription metrics. This data helps us optimize both the application and our business model.

Building a multi-tenant application requires careful planning, but the rewards are significant. You get to serve multiple customers efficiently while maintaining strong data isolation. The patterns we’ve discussed today form a solid foundation that can scale with your business.

I’d love to hear about your experiences with multi-tenant architectures. What challenges have you faced? What solutions have worked well for you? Share your thoughts in the comments below, and if you found this useful, please consider sharing it with your network.

Keywords: NestJS multi-tenant SaaS, PostgreSQL Row Level Security, Prisma multi-tenancy, JWT tenant authentication, SaaS architecture tutorial, multi-tenant database design, NestJS Prisma PostgreSQL, tenant isolation patterns, SaaS subscription management, scalable multi-tenant application



Similar Posts
Blog Image
Complete Guide to Integrating Next.js with Prisma for Full-Stack Development in 2024

Learn how to integrate Next.js with Prisma ORM for powerful full-stack applications with end-to-end type safety, seamless API routes, and optimized performance.

Blog Image
Build Complete Event-Driven Microservices Architecture with NestJS, RabbitMQ, and Redis

Learn to build scalable event-driven microservices with NestJS, RabbitMQ, and Redis. Master saga patterns, service discovery, and deployment strategies for production-ready systems.

Blog Image
Build Production-Ready GraphQL APIs: NestJS, Prisma, Redis Complete Guide with Authentication & Caching

Learn to build production-ready GraphQL APIs with NestJS, Prisma ORM, and Redis caching. Includes authentication, real-time subscriptions, and deployment.

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

Learn to build robust event-driven microservices using NestJS, RabbitMQ & Prisma. Master type-safe messaging, error handling & testing strategies.

Blog Image
Build Production-Ready Event-Driven Microservices with NestJS, NATS, and MongoDB: Complete Developer Guide

Learn to build scalable event-driven microservices using NestJS, NATS messaging, and MongoDB. Master CQRS patterns, saga transactions, and production deployment 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-driven web applications. Complete guide with setup, API routes, and best practices.