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: Building Multi-Tenant SaaS with NestJS, Prisma, and PostgreSQL Row-Level Security

Build secure multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL RLS. Learn tenant isolation, scalable architecture & performance optimization.

Blog Image
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.

Blog Image
Build High-Performance Event-Driven Microservices with NestJS, RabbitMQ, and Redis

Learn to build scalable event-driven microservices using NestJS, RabbitMQ & Redis. Master async messaging, caching, error handling & performance optimization for high-throughput systems.

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

Learn how to integrate Next.js with Prisma ORM for type-safe, scalable web applications. Discover seamless database operations and performance optimization. Start building today!

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

Learn to build scalable event-driven microservices with NestJS, RabbitMQ & Prisma. Complete guide with type-safe schemas, error handling & Docker deployment.

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

Learn to integrate Next.js with Prisma ORM for type-safe full-stack React apps. Get seamless database operations, TypeScript support, and optimized performance.