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
Complete Guide to Integrating Svelte with Supabase for Modern Full-Stack Web Applications

Learn how to integrate Svelte with Supabase for powerful full-stack web applications. Build real-time apps with authentication, databases & minimal setup.

Blog Image
Building Full-Stack TypeScript Apps: Complete Next.js and Prisma Integration Guide for Type-Safe Development

Learn to build type-safe full-stack apps with Next.js and Prisma integration. Master TypeScript database operations, schema management, and end-to-end development.

Blog Image
Build Event-Driven Microservices Architecture with NestJS, Redis, and Docker: Complete Professional Guide

Learn to build scalable event-driven microservices with NestJS, Redis, and Docker. Master inter-service communication, CQRS patterns, and deployment strategies.

Blog Image
Next.js Prisma Integration Guide: Build Type-Safe Full-Stack Apps with Modern Database Toolkit

Learn how to integrate Next.js with Prisma ORM for type-safe, scalable full-stack applications. Complete guide with setup, best practices & examples.

Blog Image
Complete Guide to Next.js Prisma Integration: Build Type-Safe Database-Driven Apps in 2024

Learn how to integrate Next.js with Prisma ORM for type-safe, database-driven web apps. Build powerful full-stack applications with seamless frontend-backend unity.

Blog Image
Type-Safe Event-Driven Microservices: NestJS, RabbitMQ, and Prisma Complete Guide

Learn to build robust event-driven microservices with NestJS, RabbitMQ & Prisma. Master type-safe architecture, distributed transactions & monitoring. Start building today!