Designing Multi-Tenant Backends That Don't Hate You at 3 AM

Schema-per-tenant, row-level security, or shared-everything? A practical walk-through of the tradeoffs I've made across three real multi-tenant systems — and the migration scars to prove it.

Designing Multi-Tenant Backends That Don't Hate You at 3 AM

Every multi-tenant system I've worked on started with the same optimistic sentence: 'we'll just add a tenant_id column to every table.' Every one of them eventually had a 3 AM incident that proved the sentence was a lie.

Shared-schema with a tenant_id column is the cheapest to start and the easiest to mess up. One missing WHERE clause and tenant A is reading tenant B's invoices. Postgres row-level security helps, but only if you actually enforce it on every connection — and only if your ORM doesn't quietly strip the session variable.

Schema-per-tenant trades runtime risk for migration pain. Isolation is wonderful until you have 800 schemas and a migration that takes four hours. I've shipped this model successfully, but only with heavy investment in migration tooling and a strict 'no online schema changes' rule.

Database-per-tenant is the safest and the most expensive. For regulated customers (health, finance) it's often the only acceptable answer. For a self-serve SaaS with 50,000 free accounts, it's financial suicide.

My current default is shared-schema with row-level security, plus a per-tenant connection pool that sets the tenant context once and refuses to operate without it. It's not glamorous, but it's the model that has woken me up the fewest times.

keep reading

More from the blog