What is Multi-Tenancy?
Multi-tenancy is an architecture where a single instance of software serves multiple customers (tenants). Each tenant's data is isolated — Clinic A can never see Clinic B's patient records — but they share the underlying infrastructure.
It's the backbone of modern SaaS. Without multi-tenancy, you'd need to deploy a separate application instance for every customer, which doesn't scale economically or operationally.
When we built DocSync, our clinic document management platform, multi-tenancy was a foundational architectural decision. Here's what we learned.
The Three Multi-Tenancy Models
1. Shared Database, Shared Schema
All tenants share the same database and tables. Each row has a TenantId column that identifies which tenant the data belongs to.
**Pros**: Simplest to implement. Lowest infrastructure cost. Easy to aggregate data across tenants.
**Cons**: Highest risk of data leakage if a query forgets the tenant filter. Noisy neighbor problems — one tenant's heavy queries affect everyone.
2. Shared Database, Separate Schemas
Each tenant gets their own database schema within a shared database instance. Tenant A's data is in schema tenant_a, Tenant B's in tenant_b.
**Pros**: Stronger isolation than shared schema. per-tenant schema migrations are possible. Easier to backup/restore individual tenants.
**Cons**: More complex connection management. Schema count can grow large. Some databases handle this better than others.
3. Separate Databases
Each tenant gets their own database instance. Complete physical isolation.
**Pros**: Strongest isolation. Per-tenant performance guarantees. Simplest to reason about security.
**Cons**: Highest infrastructure cost. Cross-tenant queries are difficult. Deployment and migration complexity multiplies with tenant count.
Our Choice: Shared Database, Shared Schema + Row-Level Security
For DocSync, we chose Model 1 (shared schema) enhanced with PostgreSQL's Row-Level Security (RLS). Here's why:
The Business Case
DocSync serves small to medium clinics. Most clinics have 1-5 doctors and handle hundreds (not millions) of patient records. Running a separate database per clinic would be economically unsustainable — the infrastructure cost would exceed what small clinics are willing to pay.
Row-Level Security (RLS)
PostgreSQL's RLS is a game-changer for shared-schema multi-tenancy. Instead of relying on application code to always include WHERE TenantId = @TenantId, we define security policies at the database level.
When a query executes, PostgreSQL automatically filters rows based on the current session's tenant context. Even if application code has a bug that omits the tenant filter, the database itself prevents cross-tenant data access.
This provides defense-in-depth — multiple layers of protection rather than a single point of failure.
Implementation Patterns
Tenant Resolution
Every incoming request needs to be mapped to a tenant. Common approaches:
- **Subdomain**: `acme-clinic.docsync.com` → extract `acme-clinic` from the hostname
- **Header**: Custom header like `X-Tenant-Id` (useful for API-first architectures)
- **JWT claim**: Include the tenant ID in the authentication token
We use a combination: the JWT token contains the tenant ID and user role, and our middleware extracts and validates this on every request.
Setting Tenant Context
Once the tenant is identified, we set the database session context:
- Middleware extracts the tenant ID from the JWT
- Before executing any query, we set PostgreSQL's session variable: `SET app.current_tenant = 'tenant_id'`
- RLS policies reference this variable to filter rows
This happens transparently — application code writes normal queries without tenant filters, and the database handles isolation automatically.
Entity Framework Core Integration
With .NET 8 and EF Core, we implement tenant filtering through:
- **Global query filters**: `modelBuilder.Entity<Patient>().HasQueryFilter(p => p.TenantId == currentTenantId)`
- **SaveChanges override**: Automatically sets the `TenantId` on all new entities
- **Audit logging**: Records which tenant and user made every change
Tenant Lifecycle Management
Onboarding
When a new clinic signs up:
- Create a tenant record with configuration (plan, features, limits)
- Create the admin user account
- Apply any tenant-specific configuration
- Send onboarding email with setup instructions
Data Isolation Verification
We have automated tests that:
- Create two test tenants
- Insert data for each tenant
- Verify that Tenant A cannot access Tenant B's data through any API endpoint
- Verify that bulk operations (reports, exports) only include the calling tenant's data
These tests run in CI/CD on every merge to main.
Tenant Offboarding
When a clinic cancels their subscription:
- Export all their data (GDPR requirement)
- Soft-delete the tenant record (grace period for reactivation)
- After grace period, permanently delete all tenant data
- Cascade deletion verified through automated tests
Performance Considerations
Index Design
In a shared-schema model, almost every query includes the tenant ID. This means:
- Composite indexes should lead with `TenantId`: `CREATE INDEX idx_patients ON patients (tenant_id, last_name)`
- Consider partial indexes for active tenants with heavy read loads
- Monitor per-tenant query performance to identify noisy neighbors
Connection Pooling
With RLS, you need to set the tenant context per connection. This interacts with connection pooling:
- Each connection from the pool needs its tenant context reset before use
- We use EF Core's connection interceptor to set `app.current_tenant` when a connection is acquired from the pool
Caching
Cache keys must include the tenant ID to prevent cross-tenant data leakage: instead of patients:recent, use tenant:ABC:patients:recent.
Monitoring and Alerting
Multi-tenant systems need per-tenant monitoring:
- **API response times by tenant**: Detect if one tenant's usage is affecting others
- **Storage usage by tenant**: Alert before hitting plan limits
- **Error rates by tenant**: Isolate issues to specific tenants
- **Active user counts**: Track growth and engagement per tenant
We built a super admin dashboard in DocSync that provides real-time visibility into all tenant metrics, with alerting for anomalies.
Lessons Learned
- **Start with RLS from day one**: Retrofitting row-level security into an existing shared database is painful. Build it in from the start.
- **Automate tenant isolation testing**: Never rely on manual verification. Automated cross-tenant access tests in CI/CD are non-negotiable.
- **Plan for the largest tenant**: Design your indexes, caching, and connection pooling for a tenant 10x larger than your current biggest.
- **Tenant-aware logging**: Every log line should include the tenant context. Without it, debugging production issues across tenants is nearly impossible.
- **Give each tenant their own admin**: Super admin is for you. Tenant admin is for your customers. Never mix the two.
Conclusion
Multi-tenant architecture is a solved problem in theory but a nuanced challenge in practice. The key is choosing the right isolation model for your business case, implementing defense-in-depth for data isolation, and investing in automated testing and monitoring. DocSync taught us that getting multi-tenancy right from the start saves enormous pain later — and that PostgreSQL's Row-Level Security is a remarkably effective tool for shared-schema isolation.