Every SaaS faces the multi-tenancy decision. There are three real patterns; each has a clear sweet spot. This post is the decision guide.
The three patterns
1. Shared schema (tenant_id column)
Every table has a tenant_id; queries filter by it.
SELECT * FROM orders WHERE tenant_id = $1 AND status = 'paid';
- Pros: Cheapest. Easy migrations. Works to ~10k tenants comfortably.
- Cons: Bug forgetting WHERE → cross-tenant leak. Noisy neighbors share resources.
- Best for: Startup phase, B2B SaaS with cooperative tenants.
2. Pooled (schema-per-tenant)
Each tenant gets a Postgres schema (tenant_42.orders); the app connects to the right schema by tenant.
SET search_path TO tenant_42;
SELECT * FROM orders;
- Pros: Stronger isolation. Per-tenant migrations possible. Easier per-tenant export.
- Cons: Schema migrations across 1000s of schemas is operational effort.
- Best for: B2B with mid-sized tenants and stronger isolation needs.
3. Per-tenant database
Each tenant gets its own database.
- Pros: Maximum isolation. Easy per-tenant backups, scaling, compliance. Cross-tenant data leaks are physically impossible.
- Cons: Most expensive (DB per tenant). Schema migrations across many DBs is real ops.
- Best for: Enterprise SaaS with strict isolation, regulated industries, large tenants.
Decision matrix
| Shared | Pooled | Per-tenant | |
|---|---|---|---|
| Up to 100 tenants | ✅ | ⚠️ | ❌ |
| 100–10k tenants | ✅ | ✅ | ⚠️ |
| 10k+ tenants | ⚠️ | ⚠️ | ❌ |
| Compliance / strong isolation | ❌ | ⚠️ | ✅ |
| Per-tenant export ease | ⚠️ | ✅ | ✅ |
| Migration cost | Low | Medium | High |
| Noisy-neighbor risk | High | Medium | None |
Postgres RLS as defense in depth
For shared-schema setups, Row-Level Security catches the “I forgot the WHERE clause” bug:
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::int);
Then in the app, set the session variable on connection check-out:
async def get_db_with_tenant(tenant_id: int):
conn = await pool.acquire()
await conn.execute("SET app.tenant_id = $1", tenant_id)
return conn
Now every query is automatically scoped. A bug that omits WHERE returns 0 rows, not other tenants’ data.
For Postgres specifics see PostgreSQL MVCC, Isolation, Locking .
Migration story
Shared schema → pooled → per-tenant is the typical evolution. Plan:
- Start shared with strict tenant_id discipline + RLS.
- Move to pooled when tenant count + isolation needs demand it.
- Move large enterprise tenants to dedicated DBs as one-offs.
- Per-tenant for everyone only if compliance forces it.
Many SaaS companies stay shared forever. Don’t migrate prematurely.
Per-tenant resource limits
Even on shared, throttle per tenant:
- API rate limit per tenant (see Design a Rate Limiter ).
- Background-job concurrency cap per tenant.
- Storage quotas.
- Compute budgets.
Without these, one runaway tenant impacts everyone.
Per-tenant features
Pair multi-tenancy with feature flags for tenant-scoped rollouts. Enterprise tenants often want different defaults.
Common mistakes
1. Forgetting WHERE clauses
Without RLS, one missed WHERE tenant_id = $1 is a breach. Always RLS.
2. Shared cache without tenant in key
Cache key user-42 collides with tenant A and tenant B’s user 42. Always tenant-scope: tenant-X:user-42.
3. Cross-tenant queries by mistake
SELECT * FROM orders without tenant filter returns everything. Defaults matter.
4. Backups not tenant-scoped
For per-tenant restore (a big tenant’s accidental delete), per-tenant backups matter.
5. Onboarding friction
If creating a new pooled-tenant or per-tenant DB takes 30 minutes of ops, signup conversion suffers. Automate onboarding.
Read this next
- PostgreSQL MVCC, Isolation, Locking
- Design a Rate Limiter
- SQLite at the Edge in 2026 — per-tenant SQLite as an alternative.
- Cloudflare Workers + D1 + Durable Objects — Durable Objects as per-tenant.
If you want a Postgres + RLS multi-tenant starter, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .