js

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

Learn to build secure multi-tenant SaaS apps with NestJS, Prisma, and PostgreSQL RLS. Master tenant isolation, JWT auth, and scalable architecture patterns.

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

I’ve been thinking a lot about multi-tenant architectures lately. Why? Because in my own SaaS projects, I kept hitting walls when scaling beyond early adopters. The moment you need to securely separate customer data while maintaining performance, traditional approaches start crumbling. That’s when I turned to PostgreSQL’s Row-Level Security combined with NestJS and Prisma - a stack that transformed how I build scalable applications.

Multi-tenancy means one application serves many customers with isolated data. There are different approaches: separate databases per tenant (secure but costly), separate schemas (moderate isolation), or shared tables with RLS (efficient and scalable). I chose RLS because it balances security with operational simplicity. How does it actually work? Let me show you.

First, our Prisma schema defines tenant-aware models. Notice the tenantId field on every tenant-specific entity:

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

The magic happens in PostgreSQL with RLS policies. We create rules that automatically filter data based on tenant context:

CREATE POLICY tenant_users_policy ON users
  FOR ALL
  USING (tenant_id = current_setting('app.current_tenant_id'));

This policy ensures users only see data where tenant_id matches their session’s tenant ID. But how do we set that context securely? Through a PostgreSQL function:

CREATE FUNCTION set_tenant_id(tenant_id text) RETURNS void AS $$
BEGIN
  PERFORM set_config('app.current_tenant_id', tenant_id, true);
END;
$$ LANGUAGE plpgsql;

In our NestJS application, we create a Prisma service that executes this function before each tenant-specific operation:

@Injectable()
export class PrismaService extends PrismaClient {
  async setTenantContext(tenantId: string) {
    await this.$executeRaw`SELECT set_tenant_id(${tenantId})`;
  }
}

Now comes the crucial part: resolving tenants from incoming requests. We use middleware to identify tenants based on subdomains:

@Injectable()
export class TenantResolutionMiddleware implements NestMiddleware {
  constructor(private tenantService: TenantService) {}
  
  async use(req: Request, res: Response, next: NextFunction) {
    const subdomain = req.subdomains[0];
    const tenant = await this.tenantService.findBySubdomain(subdomain);
    if (!tenant) throw new NotFoundException('Tenant not found');
    
    req.tenant = tenant;
    req.tenantId = tenant.id;
    next();
  }
}

We then create guards to ensure tenant context exists before accessing protected routes:

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

What about authentication? We embed the tenant ID in JWTs during login:

async login(user: User) {
  const payload = { 
    sub: user.id, 
    tenantId: user.tenantId 
  };
  return {
    access_token: this.jwtService.sign(payload),
  };
}

For tenant-aware services, we use a transaction wrapper that sets the context:

@Injectable()
export class UserService {
  constructor(private prisma: PrismaService) {}
  
  async getUsers(tenantId: string) {
    return this.prisma.withTenant(tenantId, () => {
      return this.prisma.user.findMany();
    });
  }
}

Handling custom domains? We store verified domains in our Tenant model and resolve them similarly to subdomains. But remember: always validate domain ownership before enabling!

When testing, we use Jest to simulate multi-tenant environments. Each test case creates a tenant context before operations:

test('users only see own data', async () => {
  await prisma.setTenantContext(tenantA.id);
  const users = await userService.findAll();
  expect(users).toHaveLength(3); // Only tenantA's users
});

Performance considerations? Monitor query execution times and connection pooling. I recommend PgBouncer for connection management and setting proper indexes on tenant_id columns. Watch out for N+1 queries - Prisma’s relation loading needs careful tuning.

Common pitfalls include forgetting to enable RLS on new tables (always check ALTER TABLE ... ENABLE ROW LEVEL SECURITY) and leaking tenant context between requests. Always reset tenant ID after each operation!

The beauty of this approach? We maintain a single database instance while guaranteeing data isolation. Security lives at the database level, not just application code. Have you considered how this might simplify your compliance requirements?

This architecture has served me well in production environments handling thousands of tenants. The initial setup requires careful planning, but the long-term maintenance benefits are substantial. What challenges have you faced with multi-tenancy?

If you found this useful, please share it with your network. I’d love to hear your experiences in the comments below - let’s keep the conversation going!

Keywords: multi-tenant SaaS NestJS, Prisma multi-tenancy tutorial, PostgreSQL row-level security RLS, NestJS tenant architecture, SaaS application development guide, multi-tenant database design, Prisma ORM multi-tenancy, NestJS authentication JWT tenant, subdomain routing multi-tenant, scalable SaaS backend development



Similar Posts
Blog Image
Build Full-Stack Apps Fast: Complete Svelte + Supabase Integration Guide for Modern Web Development

Learn how to integrate Svelte with Supabase for powerful full-stack web development. Build reactive UIs with PostgreSQL backend, authentication & real-time features.

Blog Image
How to Integrate Next.js with Prisma ORM: Complete Full-Stack TypeScript Development Guide

Learn how to integrate Next.js with Prisma ORM for type-safe, database-driven web applications. Build full-stack apps with seamless TypeScript support and powerful data management. Start building today!

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

Learn to build scalable GraphQL APIs with NestJS, Prisma, and Redis caching. Master authentication, real-time subscriptions, and production deployment strategies.

Blog Image
Build Real-Time Analytics Dashboard with Node.js Streams ClickHouse and Server-Sent Events Performance Guide

Learn to build a high-performance real-time analytics dashboard using Node.js Streams, ClickHouse, and SSE. Complete tutorial with code examples and optimization tips.

Blog Image
How to Build Full-Stack TypeScript Apps with Next.js and Prisma Integration

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

Blog Image
Building Production-Ready GraphQL APIs with TypeScript: Complete Apollo Server and DataLoader Implementation Guide

Learn to build production-ready GraphQL APIs with TypeScript, Apollo Server 4, and DataLoader. Master schema design, solve N+1 queries, implement testing, and deploy with confidence.