js

Complete Multi-Tenant SaaS Guide: NestJS, Prisma & PostgreSQL with Database-per-Tenant Architecture

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL. Master database isolation, dynamic connections & tenant security. Complete guide with code examples.

Complete Multi-Tenant SaaS Guide: NestJS, Prisma & PostgreSQL with Database-per-Tenant Architecture

I’ve been thinking a lot about scalable SaaS architectures lately. What separates successful platforms from struggling ones? Often it’s how they handle multiple customers securely and efficiently. Today, I’ll share practical steps for building multi-tenant applications using NestJS, Prisma, and PostgreSQL. Follow along to implement robust tenant isolation while maintaining developer sanity.

When designing multi-tenant systems, we face fundamental choices. The shared database approach adds tenant IDs to every table. It’s simple but offers weak isolation. How confident would you feel storing sensitive data this way? Instead, we’ll use database-per-tenant architecture. Each customer gets their own PostgreSQL database. This provides strong security boundaries and customization options. The trade-off? More complex connection management. Let’s solve that.

We start by setting up our NestJS project:

nest new saas-platform
cd saas-platform
npm install @nestjs/config prisma @prisma/client
npx prisma init

Our folder structure organizes concerns clearly. Key directories include tenant-manager for connection logic and modules for business features. Configuration handles environment variables:

// src/config/config.ts
export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  masterDbUrl: process.env.MASTER_DB_URL,
  tenantDbTemplate: process.env.TENANT_DB_URL.replace('{tenant}', ''),
});

The master database stores tenant metadata. We define its schema with Prisma:

// prisma/schema.prisma
model Tenant {
  id          String   @id @default(cuid())
  name        String
  subdomain   String   @unique
  dbUrl       String
  status      TenantStatus @default(ACTIVE)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

enum TenantStatus { ACTIVE SUSPENDED PENDING }

Tenant databases share a common schema template:

// prisma/tenant-template.prisma
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  tenantId  String
  createdAt DateTime @default(now())
}

The magic happens in our connection manager. We create a service that caches Prisma clients:

// src/tenant-manager/tenant.service.ts
@Injectable()
export class TenantService {
  private clients: { [key: string]: PrismaClient } = {};

  async getClient(tenantId: string): Promise<PrismaClient> {
    if (!this.clients[tenantId]) {
      const tenant = await this.masterDb.tenant.findUnique({
        where: { id: tenantId },
      });
      
      this.clients[tenantId] = new PrismaClient({
        datasources: { db: { url: tenant.dbUrl } },
      });
    }
    return this.clients[tenantId];
  }
}

Authentication must be tenant-aware. We modify Passport strategies to validate tenant context:

// src/auth/tenant.strategy.ts
@Injectable()
export class TenantStrategy extends PassportStrategy(Strategy) {
  constructor(private tenantService: TenantService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: configService.get('JWT_SECRET'),
    });
  }

  async validate(payload: { sub: string; tenantId: string }) {
    const prisma = await this.tenantService.getClient(payload.tenantId);
    return prisma.user.findUnique({ where: { id: payload.sub } });
  }
}

Request isolation is critical. We create middleware that resolves the tenant early:

// src/common/middleware/tenant.middleware.ts
export class TenantMiddleware implements NestMiddleware {
  constructor(private tenantService: TenantService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const tenantId = req.headers['x-tenant-id'] as string;
    if (!tenantId) throw new UnauthorizedException('Tenant not identified');
    
    req.prisma = await this.tenantService.getClient(tenantId);
    next();
  }
}

Database migrations need special handling. We script tenant provisioning:

// scripts/provision-tenant.ts
async function createTenant(name: string, subdomain: string) {
  const dbUrl = `${config.tenantDbTemplate}${subdomain}`;
  
  // Create physical database
  await masterDb.$executeRaw`CREATE DATABASE ${subdomain}`;
  
  // Migrate tenant schema
  const tenantPrisma = new PrismaClient({
    datasources: { db: { url: dbUrl } },
  });
  await tenantPrisma.$connect();
  await tenantPrisma.$executeRaw`CREATE SCHEMA IF NOT EXISTS ${subdomain}`;
  await tenantPrisma.$disconnect();

  // Store tenant record
  return masterDb.tenant.create({
    data: { name, subdomain, dbUrl },
  });
}

Performance requires smart pooling. We integrate Redis for caching tenant configurations:

// src/tenant-manager/tenant.service.ts
async getClient(tenantId: string): Promise<PrismaClient> {
  const cached = await redis.get(`tenant:${tenantId}`);
  if (cached) return new PrismaClient(JSON.parse(cached));

  // ...fetch from DB and cache
}

Security demands constant vigilance. We implement row-level security in PostgreSQL:

-- Enable RLS on tenant tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

-- Policy ensuring data isolation
CREATE POLICY tenant_isolation_policy ON users
USING (tenant_id = current_setting('app.current_tenant'));

Testing strategies must simulate multi-tenant environments. We use Jest to verify isolation:

// test/tenant-isolation.e2e-spec.ts
it('prevents data leakage between tenants', async () => {
  const tenantARes = await testServer(tenantA)
    .get('/users')
    .set('x-tenant-id', 'tenantA');
  
  const tenantBRes = await testServer(tenantB)
    .get('/users')
    .set('x-tenant-id', 'tenantB');

  expect(tenantARes.body).not.toEqual(tenantBRes.body);
});

Deployment considerations include connection limits. We use PgBouncer for pooling and set up monitoring with Prometheus. Alert rules watch for tenant-specific performance degradation.

Building multi-tenant systems is challenging but rewarding. Each piece must work in concert to achieve secure isolation without sacrificing developer experience. What questions do you have about scaling this further? Share your thoughts below - let’s keep learning together. If this helped you, please share it with others facing similar challenges.

Keywords: multi-tenant SaaS application, NestJS multi-tenancy tutorial, Prisma database-per-tenant architecture, PostgreSQL multi-tenant setup, tenant management system, dynamic database connections, multi-tenant authentication authorization, SaaS application security best practices, Prisma migrations multi-tenant, scalable multi-tenant NestJS



Similar Posts
Blog Image
Building Event-Driven Microservices with NestJS, RabbitMQ and TypeScript: Complete 2024 Developer Guide

Master event-driven microservices with NestJS, RabbitMQ & TypeScript. Learn architecture patterns, distributed transactions & testing strategies.

Blog Image
Complete Guide: Next.js with Prisma Integration for Type-Safe Full-Stack Development in 2024

Learn how to integrate Next.js with Prisma for full-stack type-safe development. Build modern web apps with seamless database integration and TypeScript support.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Applications

Learn to integrate Next.js with Prisma ORM for type-safe, full-stack web apps. Build database-driven applications with seamless TypeScript support and rapid development.

Blog Image
Production-Ready Event-Driven Microservices: NestJS, RabbitMQ, and Redis Architecture Guide 2024

Learn to build scalable event-driven microservices with NestJS, RabbitMQ & Redis. Covers distributed transactions, caching, monitoring & production deployment.

Blog Image
Build Real-Time Web Apps with Svelte and Supabase: Complete Developer Integration Guide

Learn to integrate Svelte with Supabase for building real-time web applications. Discover reactive components, database syncing, and authentication setup.

Blog Image
Next.js Prisma Integration Guide: Build Type-Safe Full-Stack Applications with Modern ORM

Learn how to seamlessly integrate Next.js with Prisma ORM for type-safe web apps. Build robust database-driven applications with enhanced developer experience.