Today’s Lesson
Security for Legal SaaS — Episode 27: Multi-Tenant Data Isolation
The Stakes
Your legal SaaS platform stores data for 50 law firms. Firm A's privileged litigation strategy. Firm B's M&A deal terms. Firm C's client communications about a pending regulatory investigation. If Firm A ever sees Firm B's documents, you don't have a bug — you have a lawsuit, a breach notification obligation, and likely the end of your company.
Multi-tenant data isolation is the architectural foundation that prevents this. In Episode 25, we covered ethical walls — information barriers within a single firm. Multi-tenancy is the inter-firm equivalent: every firm's data must be completely invisible to every other firm, with no possibility of cross-contamination.
Three Isolation Models
The industry has converged on three primary approaches to multi-tenant data isolation, each with different tradeoffs between security, cost, and operational complexity:1
1. Shared Database with Row-Level Security (Pool Model)
All tenants share the same database tables. Every row contains a `tenant_id` column, and access is controlled at the row level.
| Aspect | Detail |
|---|---|
| How it works | Every table has a `tenant_id` column. Every query includes a `WHERE tenant_id = :current_tenant` filter. PostgreSQL Row-Level Security (RLS) can enforce this at the database engine level — even if application code forgets the filter |
| Strengths | Lowest infrastructure cost, simplest operations, easiest to scale horizontally |
| Weaknesses | A single query bug can expose cross-tenant data. Noisy neighbour problems (one firm's heavy usage affects others) |
| Best for | Early-stage SaaS with many small tenants and cost sensitivity |
PostgreSQL RLS — which we introduced in Episode 8 — is the critical safety net here. RLS policies run at the database engine level, below the application code:2
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON documents
USING (tenant_id = current_setting('app.current_tenant')::uuid);
With this policy active, even a query that omits the `WHERE tenant_id = ...` clause will only return rows belonging to the current tenant. The database enforces isolation regardless of application bugs — a defence-in-depth pattern from Episode 4.3
2. Schema-Per-Tenant (Bridge Model)
Each tenant gets their own database schema within a shared database instance. Tables, indexes, and views are duplicated per schema.
| Aspect | Detail |
|---|---|
| How it works | Tenant A's data lives in schema `tenant_a`, Tenant B's in schema `tenant_b`. The application sets the search path to the correct schema at connection time |
| Strengths | Stronger logical isolation than shared tables. Schema-level backup and restore. Easier to customise per-tenant (additional fields, indexes) |
| Weaknesses | Schema management overhead grows with tenants. Database migrations must apply to every schema. Connection pool management becomes complex |
| Best for | Mid-tier SaaS with moderate tenant count (tens to low hundreds) and customisation needs |
PostgreSQL's Citus 12.0 introduced schema-based sharding specifically for this model — enabling horizontal scaling across nodes while preserving schema-per-tenant isolation.4
3. Database-Per-Tenant (Silo Model)
Each tenant gets their own dedicated database instance — completely separate infrastructure.
| Aspect | Detail |
|---|---|
| How it works | Tenant A connects to `db-tenant-a.rds.amazonaws.com`, Tenant B to `db-tenant-b.rds.amazonaws.com` |
| Strengths | Strongest isolation — no shared resources at all. Independent backup, restore, and migration. Per-tenant performance guarantees. Simplest compliance story |
| Weaknesses | Highest cost. Operational complexity scales linearly with tenant count. Cross-tenant analytics requires a separate data pipeline |
| Best for | Enterprise clients with strict compliance requirements, large data volumes, or contractual isolation mandates |
For legal SaaS: Many enterprise law firm clients will contractually require database-per-tenant isolation. Their security teams (and their insurers) want certainty that a vulnerability in the shared application code cannot expose their data to another firm. When a client's RFP says "dedicated database instance," they mean the silo model.5
Defence in Depth for Multi-Tenancy
No single isolation mechanism is sufficient. Defence in depth means layering multiple independent controls:6
| Layer | Control | What It Catches |
|---|---|---|
| Application code | Every query explicitly scoped by `tenant_id` | First line of defence — prevents most cross-tenant data access |
| ORM/query builder | Automatic tenant scoping middleware that injects `tenant_id` into every query | Catches queries where developers forgot the tenant filter |
| Database (RLS) | Row-level security policies enforce tenant isolation regardless of query content | Catches bugs in both application code and ORM middleware |
| Network | Tenant-specific database instances or schemas on separate network segments | Prevents lateral access if one database connection is compromised |
| Testing | Automated cross-tenant access tests in CI | Catches isolation failures before they reach production |
The testing layer is often overlooked. Your CI pipeline should include tests that explicitly attempt cross-tenant data access and verify it fails:
# CI test: ensure Tenant A cannot see Tenant B's data
def test_cross_tenant_isolation():
# Set context to Tenant A
set_tenant_context(tenant_a_id)
# Create a document as Tenant A
doc = create_document(content="Tenant A privileged communication")
# Switch context to Tenant B
set_tenant_context(tenant_b_id)
# Verify Tenant B cannot access Tenant A's document
assert get_document(doc.id) raises NotFoundError
assert search_documents("privileged communication") returns []
Tenant Context Propagation
The most common source of multi-tenancy bugs is tenant context propagation — ensuring that every component in the request chain knows which tenant the current request belongs to.7
The tenant context must flow through:
- Authentication — the JWT token or session contains the user's `tenant_id`
- API middleware — extracts `tenant_id` from the token and sets it in the request context
- Service layer — passes `tenant_id` to every database query and external service call
- Database connection — sets `app.current_tenant` for RLS enforcement
- Background jobs — async tasks (email notifications, report generation) must carry the `tenant_id` from the originating request
- Logging — every log entry includes `tenant_id` for debugging and audit
The background job trap: A scheduled report generation job runs outside a user request context. If it doesn't explicitly set the tenant context, it may default to no tenant filter — and the report includes data from all tenants. This is a real and documented failure mode. Every background job must explicitly set and verify its tenant context before accessing data.7
The Snowflake Incident: A Cautionary Tale
The 2024 Snowflake breach affected over 165 organisations, including AT&T, Ticketmaster, and Santander.8 Critically, Snowflake's core infrastructure was never compromised. The attackers used stolen credentials — harvested from infostealer malware dating back to 2020 — to log into individual Snowflake accounts that lacked MFA.
The lesson for multi-tenant SaaS is twofold:
- Tenant-level security policy enforcement matters. If your platform allows tenants to opt out of MFA, some will — and those are the accounts that get breached. Enforce baseline security requirements across all tenants.
- Credential hygiene is a multi-tenant concern. Even with perfect data isolation, a compromised tenant account gives the attacker access to that tenant's data. Combine isolation with strong authentication (Episode 21) and SSO (Episode 22).9
Ethical Walls vs. Multi-Tenancy: The Complete Picture
With Episode 25 fresh in memory, here's the complete access control picture for legal SaaS:
| Concept | Scope | Mechanism | Failure Impact |
|---|---|---|---|
| Multi-tenant isolation | Between firms | Database isolation (RLS, schema, silo) | Firm A sees Firm B's data — catastrophic |
| Ethical walls | Within a firm | Application-layer deny rules, search filtering | Conflicted lawyer sees restricted matter — ethics violation |
| Matter scoping | Within a firm | User-to-matter assignment checks | Unassigned user accesses matter — privilege breach |
| RBAC | Within a firm | Role-to-permission mapping | User exceeds their access level — unauthorised action |
Each layer is independent. A bug in ethical wall enforcement should never compromise multi-tenant isolation. A misconfigured role should never expose data across tenants. Independence between layers is the essence of defence in depth.
What's Next
Episode 28 covers Encryption at Rest vs. in Transit — how to protect data when it's stored on disk, when it's moving between services, and the envelope encryption pattern that makes key management practical.
Sources & Further Reading
Sources & references
- AWS, Multi-Tenant Data Isolation with PostgreSQL Row Level Security — RLS patterns for SaaS.
- DZone, Multi-Tenant Data Isolation and Row Level Security — implementation patterns.
- Redis, Data Isolation in Multi-Tenant SaaS: Architecture & Security Guide — comprehensive isolation model comparison.
- Citus Data, Citus 12: Schema-Based Sharding for SaaS — horizontal scaling for schema-per-tenant.
- Hunchbite, Multi-Tenant SaaS Architecture: Row-Level Security vs. Schema-Per-Tenant — architectural comparison.
- Aloa, How to Build a Multi-Tenant SaaS Database — comprehensive implementation guide.
- Dev.to, Multi-Tenant SaaS Data Isolation: Row-Level Security, Tenant Scoping, and Plan Enforcement with Prisma — tenant context propagation patterns.
- Cloud Security Alliance, Unpacking the 2024 Snowflake Data Breach — breach analysis.
- Push Security, Snowflake: Looking Back on 2024's Landmark Security Event — lessons learned.
- OneUptime, How to Design a Multi-Tenant Data Isolation Strategy on Azure SQL Database — Azure-specific guidance.
- Wikipedia, Snowflake Data Breach — timeline and impact.