Patterns multi-tenancy avec Drizzle ORM pour bâtir un SaaS B2B en 2026.
Voir notre guide Drizzle.
3 approches multi-tenant
- Database par tenant : isolation maximum, complexité ops
- Schema par tenant : isolation logique, mêmes ressources DB
- Row-level (tenant_id column) : simple, scaling facile
Approche recommandée : tenant_id + RLS
// schema.ts
export const tenants = pgTable("tenants", {
id: serial("id").primaryKey(),
name: varchar("name", { length: 100 }),
});
export const customers = pgTable("customers", {
id: serial("id").primaryKey(),
tenant_id: integer("tenant_id").references(() => tenants.id).notNull(),
email: varchar("email", { length: 255 }).notNull(),
name: varchar("name", { length: 100 }),
}, (table) => ({
tenantIdx: index("customers_tenant_idx").on(table.tenant_id),
uniqueEmail: uniqueIndex("customers_tenant_email").on(table.tenant_id, table.email),
}));RLS Postgres pour isolation
-- Migration custom (drizzle ne génère pas RLS)
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON customers
USING (tenant_id = current_setting('app.tenant_id')::integer);
-- Dans l'app, à chaque requête :
SET app.tenant_id = '42';
SELECT * FROM customers; -- ne voit que les rows du tenant 42Wrapper Drizzle avec tenant context
import { sql } from "drizzle-orm";
async function withTenant<T>(tenantId: number, fn: () => Promise<T>): Promise<T> {
return await db.transaction(async (tx) => {
await tx.execute(sql`SET LOCAL app.tenant_id = ${tenantId}`);
return await fn();
});
}
// Usage
await withTenant(42, async () => {
return await db.select().from(customers);
});Audit log multi-tenant
Pour conformité, ajouter une table audit_logs avec tenant_id et trigger qui enregistre toute modification.