Multi-tenancy decisions feel irreversible, which is why teams agonise over them. Good news: there's a sensible default, and a clear upgrade path when you outgrow it.
Start with shared schema, tenant_id everywhere
One database, one schema, a tenant_id column on every table, enforced by a default scope in your ORM. It's operationally simple, cheap, and scales further than most products ever need. The non-negotiable: a middleware layer that sets the current tenant once per request, so no query can forget the filter.
- Add a composite index (tenant_id, ...) on every hot query path
- Write an automated test that proves cross-tenant reads are impossible
- Tag background jobs with tenant context explicitly - queues are where leaks happen
When to reach for stronger isolation
Enterprise buyers, regulated data or noisy-neighbour performance problems justify schema-per-tenant or database-per-tenant. The cost is real: migrations multiply, connection pools fragment, and your ops burden scales with tenant count. Adopt it for the tenants that demand it - a hybrid where 3 enterprise tenants get dedicated databases while thousands share one is common and sane.
Design the seams early
Even on shared schema, route all tenant resolution through one module and all data access through scoped repositories. If isolation requirements change, you refactor one seam - not four hundred call sites.
Billing and limits are tenancy too
Per-tenant rate limits, feature flags and usage metering belong in the same tenant context object. Retrofitting metering after enterprise deals arrive is far more painful than carrying a thin layer from day one.