When architecting a Software-as-a-Service (SaaS) backend, a core architectural decision is how to store tenant data. The choice impacts security, backup strategies, and scale costs.
Let's dissect the three primary isolation methodologies:
1. Database-Per-Tenant (Shared Process, Isolated DB)
Every tenant gets their own physical database.
- Pros: Complete data isolation. Ideal for medical or financial systems requiring strict compliance.
- Cons: High resource overhead. Dynamic database creation is complex, and migrations must be run on multiple databases.
2. Schema-Per-Tenant (Shared DB, Isolated Schema)
Tenants share a database process but write to isolated schemas (e.g. PostgreSQL schemas).
- Pros: Relatively easy to migrate, moderate isolation.
- Cons: Still hits connection pool limitations as schemas grow into the thousands.
3. Shared DB, Shared Schema (Row-Level Isolation)
Tenants share tables. Every row includes a tenant_id column.
- Pros: Highly cost-effective. Easily handles thousands of tenants.
- Cons: Risk of data leaks if query writers forget to filter by
tenant_id.
Implementation: Dynamic Connection Contexts
In Node.js, we can write a middleware that intercepts the domain or headers, selects the correct database pool, and sets it on the request context:
app.use((req, res, next) => {
const tenantId = req.headers['x-tenant-id'];
if (!tenantId) return res.status(400).send('Tenant ID required');
req.db = getTenantDBConnection(tenantId);
next();
});