js

Build Multi-Tenant SaaS with NestJS, Prisma, PostgreSQL RLS: Complete Tutorial

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma, and PostgreSQL RLS. Covers tenant isolation, dynamic schemas, and security best practices.

Build Multi-Tenant SaaS with NestJS, Prisma, PostgreSQL RLS: Complete Tutorial

I’ve been thinking about building scalable SaaS applications recently. Many developers struggle with tenant isolation and security when multiple clients share the same application instance. Today, I’ll show you how to create a robust multi-tenant system using NestJS, Prisma, and PostgreSQL’s Row-Level Security. Let’s build this together.

Why focus on this stack? NestJS provides a solid TypeScript foundation, Prisma simplifies database interactions, and PostgreSQL RLS offers database-enforced security. This combination handles data isolation at multiple layers. Ever wonder how large SaaS platforms prevent data leaks between customers? The secret often lies in proper RLS implementation.

First, we initialize our project:

nest new saas-app
npm install @prisma/client prisma
npx prisma init

Our architecture uses a hybrid approach. Smaller tenants share tables with RLS protection, while larger ones get dedicated schemas. Here’s our core configuration interface:

// tenant.types.ts
export interface TenantContext {
  tenantId: string;
  tenantSlug: string;
  schemaName?: string;
}

export interface MultiTenantConfig {
  strategy: 'shared' | 'schema-per-tenant' | 'hybrid';
  maxTenantsPerInstance: number;
  enableRLS: boolean;
}

For database design, we extend Prisma’s capabilities with multi-schema support. Notice the tenantId field on all tenant-specific models:

// schema.prisma
model Project {
  id        String   @id @default(uuid())
  tenantId  String
  name      String
  tasks     Task[]
  
  @@map("projects")
}

model Task {
  id          String   @id @default(uuid())
  tenantId    String
  projectId   String
  title       String
  
  @@map("tasks")
}

PostgreSQL RLS is where the real isolation happens. We enable policies that restrict data access based on tenant context:

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation_policy ON projects
FOR ALL USING (tenant_id = current_setting('app.current_tenant'));

In NestJS, we implement tenant-aware middleware to capture context from subdomains or JWT tokens:

// tenant.middleware.ts
@Injectable()
export class TenantMiddleware implements NestMiddleware {
  constructor(private prisma: PrismaService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const tenantSlug = req.headers['x-tenant-id'] as string;
    const tenant = await this.prisma.tenant.findUnique({ 
      where: { slug: tenantSlug } 
    });
    
    req.tenant = {
      id: tenant.id,
      schemaName: tenant.schemaName
    };
    
    next();
  }
}

Managing database connections efficiently is critical. Our dynamic Prisma service switches clients based on tenant context:

// prisma.service.ts
@Injectable()
export class PrismaService implements OnModuleInit {
  private tenantClients: Map<string, PrismaClient> = new Map();

  async getClient(tenantId: string) {
    if (!this.tenantClients.has(tenantId)) {
      const client = new PrismaClient({
        datasources: { db: { url: this.buildTenantUrl(tenantId) } 
      });
      await client.$connect();
      this.tenantClients.set(tenantId, client);
    }
    return this.tenantClients.get(tenantId);
  }
}

Security is non-negotiable. We combine RLS with application-level guards:

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

When onboarding new tenants, we automate schema creation and RLS setup:

// tenant.service.ts
async onboardTenant(name: string) {
  const schemaName = `tenant_${generateSlug(name)}`;
  await this.createSchema(schemaName);
  await this.enableRLS(schemaName);
  return this.prisma.tenant.create({
    data: { name, schemaName }
  });
}

Performance matters at scale. We use connection pooling with PgBouncer and optimize queries:

// project.service.ts
async getProjects(tenantId: string) {
  const prisma = await this.prisma.getClient(tenantId);
  return prisma.project.findMany({
    where: { tenantId },
    include: { tasks: { take: 5 } }
  });
}

Testing requires special attention. We verify isolation by simulating multi-tenant access:

// project.e2e-spec.ts
it('prevents cross-tenant access', async () => {
  const tenant1 = await createTenant();
  const tenant2 = await createTenant();
  
  const res = await request(app)
    .get('/projects')
    .set('X-Tenant-Id', tenant1.id);
  
  expect(res.body).not.toContainEqual(
    expect.objectContaining({ tenantId: tenant2.id })
  );
});

Common pitfalls? Schema management complexity tops the list. We mitigate this with automated migration scripts and monitoring. Another challenge is connection pooling - too many clients can exhaust database resources. Our dynamic client management helps here.

For deployment, we use Docker containers with environment-specific RLS policies. We monitor tenant databases separately and alert on unusual activity.

What if you need more isolation? Consider dedicated databases for enterprise clients. Though more expensive, it provides maximum separation.

I’ve found this architecture handles most SaaS scenarios effectively. The hybrid approach balances security with operational efficiency. Give it a try for your next multi-tenant project.

If you found this useful, share it with your network. Have questions or suggestions? Let’s discuss in the comments - I’ll respond to every query.

Keywords: multi-tenant SaaS NestJS, Prisma PostgreSQL multi-tenancy, Row-Level Security RLS PostgreSQL, NestJS Prisma multi-tenant architecture, SaaS application development tutorial, PostgreSQL tenant isolation strategies, NestJS middleware tenant context, Prisma dynamic client management, multi-tenant database design patterns, SaaS security best practices



Similar Posts
Blog Image
Build Distributed Task Queue: BullMQ, Redis, TypeScript Guide for Scalable Background Jobs

Learn to build robust distributed task queues with BullMQ, Redis & TypeScript. Handle job priorities, retries, scaling & monitoring for production systems.

Blog Image
Building Event-Driven Microservices with NestJS, RabbitMQ, and MongoDB: Complete Professional Guide

Learn to build scalable event-driven microservices using NestJS, RabbitMQ & MongoDB. Master CQRS, event sourcing, and distributed systems. Start coding now!

Blog Image
Build High-Performance GraphQL API with NestJS, Prisma, and Redis Caching Complete Guide

Build a high-performance GraphQL API with NestJS, Prisma & Redis caching. Learn DataLoader patterns, auth, and optimization techniques for scalable APIs.

Blog Image
Build Real-time Web Apps: Complete Svelte and Supabase Integration Guide for Modern Developers

Learn to integrate Svelte with Supabase for building high-performance real-time web applications. Discover seamless data sync, authentication, and reactive UI updates.

Blog Image
Build Type-Safe Event-Driven Architecture with TypeScript, Node.js, and Redis Streams

Learn to build type-safe event-driven architecture with TypeScript, Node.js & Redis Streams. Complete guide with code examples, scaling tips & best practices.

Blog Image
Build Multi-Tenant SaaS Applications with NestJS, Prisma, and PostgreSQL Row-Level Security

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma, and PostgreSQL RLS. Complete guide with secure tenant isolation and database-level security. Start building today!