Designing Multi-Tenant Systems Without Creating a Data Leak Nightmare
Multi-tenancy is one of those architectural decisions that feels reversible in the beginning and absolutely isn't by the time you hit 50 customers. Pick wrong and you're either migrating data for weeks or explaining to a client why they can see someone else's dashboard.
This post breaks down the three main approaches, when each makes sense, and the guardrails you need regardless of which path you choose.
The Three Tenancy Models
1. Shared Table — One Big Pool
Every tenant's data lives in the same tables, differentiated by a tenant_id column. It's the easiest to start with and the hardest to secure correctly.
SELECT * FROM orders
WHERE tenant_id = 'tenant_abc'
AND deleted_at IS NULL;The risk is obvious: one missing WHERE clause and data crosses tenant boundaries. Row-Level Security (RLS) in PostgreSQL helps enforce this at the database layer rather than relying on application-level discipline.
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::text);When to use it: B2B SaaS with hundreds of small tenants, simple data models, and strong RLS policies. Great for MVP stages but you'll want to monitor query performance as tenant count grows.
2. Schema-Per-Tenant — Separated by Default
Each tenant gets their own schema (namespace) within a shared database. PostgreSQL schemas make this practical without spinning up separate databases.
CREATE SCHEMA tenant_abc;
CREATE SCHEMA tenant_xyz;
SET search_path TO tenant_abc, public;Isolation is baked in — a query on tenant_abc.orders can never accidentally return tenant_xyz data. Migration tooling (think Prisma migrations with dynamic schema targeting) becomes more complex, and connection pooling needs to account for schema switching.
When to use it: Medium-scale B2B with 50–500 tenants, complex data models, compliance requirements (HIPAA, SOC2). The added operational complexity is worth the isolation guarantees.
3. Database-Per-Tenant — Full Isolation, Full Cost
Each tenant gets their own database instance. Maximum isolation, maximum operational overhead.
This approach shines when tenants have vastly different data volumes, when you need per-tenant backup/restore capabilities, or when regulatory requirements mandate physical separation. It fails when you're managing 10,000 small tenants and spending all your time on connection management.
When to use it: Enterprise customers with compliance demands, when tenant data volume varies by orders of magnitude, or when you need per-tenant failover capabilities.
What Breaks in Practice
Three things consistently break in multi-tenant systems regardless of the model you choose:
- Connection pooling: A single connection pool shared across tenants means one noisy tenant starves everyone else. Per-tenant pools solve this but increase memory pressure. The middle ground is weighted pool sizing with circuit breakers.
- Migration hell: Schema-per-tenant means running migrations N times. Tools like
pgrolland custom migration runners with dry-run capabilities are essential. Always test against a representative tenant before rolling to all. - Backup strategy: Database-per-tenant requires orchestrating N backup jobs. Shared table means one backup covers everything but restore is all-or-nothing. Schema-per-tenant is the worst — you need schema-level backup tooling that most providers don't support natively.
Practical Guardrails
Regardless of your tenancy model, these patterns prevent the most common disasters:
- Never trust the application layer alone. RLS, schema isolation, or separate databases — pick at least two layers of tenant isolation. A bug in the ORM should never be the only thing between a client and someone else's data.
- Tenant-aware observability. Every log line, every metric, every trace should include
tenant_id. When a tenant reports slowness at 3 PM, you need to know whether it's their data volume or a noisy neighbor. - Rate limit per tenant, not per user. A single tenant with 200 automated users shouldn't degrade the experience for everyone else. Implement tenant-level rate limiting above user-level limits.
A convenience-first approach to tenancy always feels right until it isn't. The cost of retrofitting isolation is almost always higher than the cost of building it correctly from day one.
Redis Caching That Doesn't Rot Your System From the Inside
Caching is easy until invalidation turns your app into a liar. This breaks down practical TTL strategy, namespaced keys, and targeted invalidation.
Building RAG Pipelines That Actually Work in Production
Chunking strategies, embedding selection, retrieval re-ranking, and why naive RAG falls apart at scale without careful pipeline design.
How I Structure Production APIs So They Don't Collapse Under Growth
Controllers, services, validation boundaries, auth layers, cache placement, and why most beginner backends become unreadable after 3 months.