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
Build Real-Time Web Apps: Complete Svelte and Supabase Integration Guide for Modern Developers

Build real-time web apps with Svelte and Supabase integration. Learn to combine reactive frontend with backend-as-a-service for live updates and seamless development.

Blog Image
Complete Guide: Integrating Next.js with Prisma ORM for Type-Safe Database Applications in 2024

Learn how to integrate Next.js with Prisma ORM for type-safe, full-stack applications. Build database-driven apps with seamless data management and enhanced developer experience.

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, full-stack applications. Build faster web apps with seamless database operations. Start today!

Blog Image
Build Type-Safe Real-Time APIs with GraphQL Subscriptions TypeScript and Redis Complete Guide

Learn to build production-ready real-time GraphQL APIs with TypeScript, Redis pub/sub, and type-safe resolvers. Master subscriptions, auth, and scaling.

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

Learn how to integrate Next.js with Prisma ORM for type-safe, database-driven apps. Build full-stack applications with seamless data flows and improved developer experience.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Database Management

Learn how to integrate Next.js with Prisma ORM for type-safe database operations, seamless migrations, and full-stack TypeScript development. Build faster apps today!