Tenant Isolation Strategies and Trade-offs
Compare shared-schema, schema-per-tenant, and database-per-tenant isolation models for SaaS workloads.
Why Tenant Isolation Matters
In a multi-tenant SaaS, many customers (tenants) share one running FastAPI application. The central question is: how isolated is each tenant's data?
Isolation affects four things you must trade off constantly:
- Security & blast radius — can a bug leak Tenant A's rows to Tenant B?
- Cost — how much infra does each tenant consume?
- Operational complexity — migrations, backups, restores.
- Per-tenant customization — can one tenant get extra columns or a custom schema?
Three canonical models exist: shared-schema, schema-per-tenant, and database-per-tenant. The rest of this lesson compares them for FastAPI workloads.
Shared-Schema: One Table, a tenant_id Column
The simplest model: every tenant's rows live in the same tables, distinguished by a tenant_id column. Every query must filter on it.
This is the cheapest and most scalable option for thousands of small tenants, but isolation is purely logical — one missing WHERE tenant_id = ... leaks data across tenants.
Below is the typical SQLAlchemy model shape. Note the indexed tenant_id on every tenant-owned table.
from sqlalchemy import String, Integer, ForeignKey, Index
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Invoice(Base):
__tablename__ = "invoices"
id: Mapped[int] = mapped_column(primary_key=True)
tenant_id: Mapped[str] = mapped_column(String, index=True)
amount_cents: Mapped[int] = mapped_column(Integer)
customer_email: Mapped[str] = mapped_column(String)
# Composite index: nearly every query filters by tenant first
__table_args__ = (Index("ix_invoices_tenant_id", "tenant_id"),)All lessons in this course
- Tenant Isolation Strategies and Trade-offs
- Tenant Context Resolution and Middleware
- Row-Level Security and Data Partitioning
- Usage Metering, Quotas and Billing Hooks