TypeORM Multi-Tenancy with PostgreSQL
Choosing a Multi-Tenancy Strategy
SaaS Starter includes a production-ready multi-tenancy implementation. See the multi-tenancy feature page.
Multi-tenancy is the foundation of any SaaS product. Your tenants share infrastructure but their data must remain isolated. With TypeORM and PostgreSQL, you have three options, each with clear trade-offs.
Strategy 1: Row-Level Isolation
The simplest approach. All tenants share one database and one schema. Every table has a tenantId column, and every query filters by it.
@Entity()
export class Project {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
tenantId: string;
@Column()
name: string;
}
Enforce tenant filtering globally with a subscriber:
@EventSubscriber()
export class TenantSubscriber implements EntitySubscriberInterface {
beforeInsert(event: InsertEvent<any>) {
if (event.entity && this.tenantContext.tenantId) {
event.entity.tenantId = this.tenantContext.tenantId;
}
}
}
Pros: Simple migrations, easy to manage, lowest infrastructure cost. Cons: One missing WHERE clause leaks data across tenants. Noisy-neighbor risk on large tables.
Strategy 2: Schema-Level Isolation
Each tenant gets their own PostgreSQL schema within the same database. This provides stronger isolation while keeping a single database connection.
@Injectable({ scope: Scope.REQUEST })
export class TenantConnectionService {
async getConnection(tenantId: string): Promise<DataSource> {
const schema = `tenant_${tenantId}`;
await this.dataSource.query(`SET search_path TO "${schema}"`);
<div class="not-prose my-8 rounded-xl border border-primary-200 dark:border-primary-800 bg-primary-50 dark:bg-primary-900/20 p-6">
<p class="text-base font-semibold text-primary-900 dark:text-primary-100 mb-1">Cloudrix SaaS Starter ships with this pre-configured and tested.</p>
<p class="text-sm text-primary-700 dark:text-primary-300 mb-3">Skip weeks of boilerplate — auth, payments, multi-tenancy, and deployment included out of the box.</p>
<a href="https://demo.cloudrix.io" class="inline-flex items-center text-sm font-medium text-primary-600 dark:text-primary-400 hover:text-primary-500">Try the live demo →</a>
</div>
return this.dataSource;
}
}
Run migrations per schema:
async migrateTenant(tenantId: string) {
const schema = `tenant_${tenantId}`;
await this.dataSource.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
await this.dataSource.query(`SET search_path TO "${schema}"`);
await this.dataSource.runMigrations();
}
Pros: Strong isolation, PostgreSQL enforces boundaries, shared connection pool. Cons: Schema count can grow large. Migrations must run per tenant.
Strategy 3: Database-Level Isolation
Each tenant gets a completely separate database. Maximum isolation, maximum overhead.
@Injectable()
export class TenantDatabaseService {
private connections = new Map<string, DataSource>();
async getConnection(tenantId: string): Promise<DataSource> {
if (!this.connections.has(tenantId)) {
const ds = new DataSource({
type: 'postgres',
database: `saas_${tenantId}`,
// ... connection options
});
await ds.initialize();
this.connections.set(tenantId, ds);
}
return this.connections.get(tenantId);
}
}
Pros: Complete isolation, per-tenant backup and restore, no noisy neighbors. Cons: Connection pool per tenant, expensive at scale, complex deployment.
Row-Level Security with PostgreSQL Policies
For row-level tenancy, PostgreSQL's built-in Row-Level Security (RLS) adds a database-enforced safety net:
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.tenant_id')::uuid);
Set the tenant context on each request:
async onModuleInit() {
this.dataSource.query(`SET app.tenant_id = '${tenantId}'`);
}
Even if your application code has a bug, PostgreSQL will refuse to return rows belonging to other tenants.
Which Strategy Should You Pick?
For most early-stage SaaS products, row-level with RLS is the right choice. It keeps infrastructure simple and costs low while providing database-enforced isolation. Move to schema-level when you have enterprise customers who contractually require it.
Get Started Today
All of these patterns are fully implemented in SaaS Starter. Skip 2-6 weeks of setup.
- Free version: GitHub
- Pro ($249): Get SaaS Starter Pro