Schema-per-Tenant Database Connections
Switch database schemas or connections dynamically based on the resolved tenant.
Schema-per-Tenant: The Big Picture
In a multi-tenant API, every tenant's data must stay isolated. The schema-per-tenant model keeps one physical database but gives each tenant its own PostgreSQL schema (e.g. tenant_acme, tenant_globex). Tables have identical structures across schemas.
- Pool of tables (shared schema): one set of tables, isolation by a
tenant_idcolumn. Simple, but leaks are one missed WHERE clause away. - Schema-per-tenant: stronger isolation, easy per-tenant backup, but you must switch the active schema per request.
- Database-per-tenant: maximum isolation, heaviest operational cost.
This lesson focuses on dynamically routing each request to the correct schema or connection once the tenant has been resolved.
Resolving the Tenant per Request
Before you can switch schemas, you need the tenant. Resolution typically comes from a subdomain, a header, or a JWT claim. A lightweight middleware extracts it and attaches it to the request so downstream providers can read it.
Keep resolution dumb and cheap here; validation of whether the tenant exists happens when you build the connection.
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class TenantMiddleware implements NestMiddleware {
use(req: Request, _res: Response, next: NextFunction) {
// Prefer an explicit header; fall back to subdomain.
const headerTenant = req.headers['x-tenant-id'] as string | undefined;
const host = req.headers.host ?? '';
const subdomain = host.split('.')[0];
const tenantId = headerTenant ?? subdomain;
(req as any).tenantId = tenantId;
next();
}
}All lessons in this course
- Tenant Resolution via Middleware and AsyncLocalStorage
- Schema-per-Tenant Database Connections
- Building Configurable Dynamic Modules
- Request-Scoped Providers and Their Trade-offs