Multi-tenant SaaS database isolation: separate DB, schema, or tenant_id?
Question
I'm building a new SaaS and need to isolate customer (tenant) data. There are three options: a separate DB per customer (database-per-tenant), separate schemas in the same DB (schema-per-tenant), or a `tenant_id` column in a single DB (shared). Considering security, backup ease, cost and schema migration, how do I choose the most optimal architecture that scales to 10,000 active customers?
Answer
Short answer: at 10,000 tenants, the pragmatic answer is a shared DB with tenant_id everywhere + isolation buried in Postgres Row-Level Security.
Let me first say why I rule out the other two, because at this count the decision comes down to operations:
- Database-per-tenant doesn’t scale at 10k. A separate DB per tenant means 10,000 migrations, 10,000 connection pools, 10,000 backups. It gives perfect isolation but you’ll be crushed under the operational load. This only makes sense for a small number of large customers.
- Schema-per-tenant is a middle ground but also strains. Separate schemas in one DB bridge isolation and management; but at 10k schemas the catalog bloats and migrations still run N times. Fine up to a few hundred tenants, not ten thousand.
- Shared DB +
tenant_ideverywhere. One schema,tenant_idon every table. Backups and migrations are simplest here — one DB, one migration. What you trade away is security, and you buy that back with RLS. - Bury isolation in the DB with RLS. With Postgres Row-Level Security, every query is automatically scoped to
tenant_id. Let isolation live in the database itself, not in the assumption that “the developer hopefully scoped every query”. This is your real defense against leaks. - Add an ORM scope + indexing/partitioning. Put a tenant scope at the ORM layer (defense-in-depth), and index/partition hot tables by
tenant_id. And if a “whale” tenant grows, keep the path open from day one to promote it to its own DB.
Bottom line: I’d build a shared DB + tenant_id everywhere + Postgres RLS. Backups and migrations are simplest in the shared model; security is the trade-off you pay, but you buy it back with RLS. Make the ORM scope your second line of defense, partition hot tables by tenant_id, and leave the door open to move a growing tenant to its own DB. I explain why the data architecture is built this way on sade.dev.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.