Skip to main content
fastapi-tenancy

fastapi-tenancy

Production-ready multi-tenancy for FastAPI

Schema · Database · RLS · Hybrid isolation — fully async, fully typed

PyPI version Python CI Coverage Downloads Monthly Docs License: MIT Ruff uv


Why fastapi-tenancy?

Every SaaS product built with FastAPI eventually hits the same walls:

  • Where does tenant context live without leaking between requests or background tasks?
  • How do you enforce schema or database isolation without boilerplate in every route?
  • How do you support different isolation tiers for different customer plans?
  • How do you run per-tenant Alembic migrations without a custom script every time?

fastapi-tenancy solves all of these — one library, four isolation strategies, zero per-route boilerplate.


Features

Category What's included
Isolation Schema-per-tenant, database-per-tenant, PostgreSQL RLS, Hybrid (mix by tier)
Resolution Header, subdomain, URL path, JWT claim — or bring your own
Storage SQLAlchemy async (PostgreSQL, MySQL, SQLite, MSSQL), Redis, in-memory
Context contextvars-based — propagates through background tasks and streaming
Middleware Raw ASGI (not BaseHTTPMiddleware) — zero buffering, correct ContextVar propagation
Caching In-process LRU + TTL L1 cache wired into every request; Redis L2
Encryption Fernet/HKDF field-level encryption for database_url and _enc_* metadata
Security JWT algorithm-confusion prevention, SQL-injection-safe DDL, anti-enumeration errors
Migrations Per-tenant Alembic runner with concurrency control
Observability get_metrics() — L1 cache hit rate, engine pool size
Typing py.typed, strict mypy, Pydantic v2 frozen models throughout

Installation

# Minimal
pip install fastapi-tenancy

# PostgreSQL
pip install "fastapi-tenancy[postgres]"

# Full stack — PostgreSQL + Redis + JWT + Alembic
pip install "fastapi-tenancy[full]"
All available extras
Extra Driver installed
postgres asyncpg
sqlite aiosqlite
mysql aiomysql
mssql aioodbc
redis redis[asyncio]
jwt PyJWT
migrations alembic
full all of the above

Quick Start

from fastapi import FastAPI, Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from fastapi_tenancy import TenancyManager, TenancyConfig
from fastapi_tenancy.storage.database import SQLAlchemyTenantStore
from fastapi_tenancy.middleware.tenancy import TenancyMiddleware
from fastapi_tenancy.dependencies import make_tenant_db_dependency

# 1. Configure — all fields overridable via TENANCY_* env vars
config = TenancyConfig(
    database_url="postgresql+asyncpg://user:pass@localhost/myapp",
    resolution_strategy="header",   # read tenant from X-Tenant-ID header
    isolation_strategy="schema",    # one PostgreSQL schema per tenant
)

# 2. Wire
store   = SQLAlchemyTenantStore(config.database_url)
manager = TenancyManager(config, store)

# 3. Mount
app = FastAPI(lifespan=manager.create_lifespan())
app.add_middleware(TenancyMiddleware, manager=manager)

# 4. Use — zero per-route boilerplate
get_tenant_db = make_tenant_db_dependency(manager)

@app.get("/orders")
async def list_orders(db: AsyncSession = Depends(get_tenant_db)):
    # db is already scoped to the current tenant's schema
    result = await db.execute(select(Order))
    return result.scalars().all()

Isolation Strategies

Schema isolation — PostgreSQL / MSSQL

config = TenancyConfig(
    database_url="postgresql+asyncpg://...",
    isolation_strategy="schema",
    schema_prefix="t_",
)

Database isolation — all dialects

config = TenancyConfig(
    database_url="postgresql+asyncpg://.../master",
    isolation_strategy="database",
    database_url_template="postgresql+asyncpg://.../{database_name}",
)

Row-Level Security — PostgreSQL

config = TenancyConfig(
    database_url="postgresql+asyncpg://...",
    isolation_strategy="rls",
)

Hybrid — mix strategies by tier

config = TenancyConfig(
    database_url="postgresql+asyncpg://...",
    isolation_strategy="hybrid",
    premium_isolation_strategy="schema",
    standard_isolation_strategy="rls",
    premium_tenants=["enterprise-co", "whale-customer"],
)

Tenant Resolution

# HTTP header (default: X-Tenant-ID)
TenancyConfig(resolution_strategy="header", tenant_header_name="X-Tenant-ID")

# Subdomain: acme.example.com -> "acme"
TenancyConfig(resolution_strategy="subdomain", domain_suffix=".example.com")

# URL path: /t/acme/orders -> "acme"
TenancyConfig(resolution_strategy="path", path_prefix="/t/")

# JWT claim from Bearer token
TenancyConfig(resolution_strategy="jwt", jwt_secret="...", jwt_tenant_claim="tenant_id")

All failure modes — missing header, invalid format, unknown tenant — return the same generic "Tenant not found" to prevent identifier enumeration.


Tenant Lifecycle

# Provision — creates schema/database + tables + stores metadata
tenant = await manager.register_tenant(
    identifier="acme-corp",
    name="Acme Corporation",
    metadata={"plan": "enterprise", "max_users": 500},
    app_metadata=Base.metadata,
)

await manager.suspend_tenant(tenant.id)
await manager.activate_tenant(tenant.id)
await manager.delete_tenant(tenant.id)  # soft delete by default

Field-level Encryption

config = TenancyConfig(
    ...,
    enable_encryption=True,
    encryption_key="your-secret-key-at-least-32-chars!",
)

tenant = await manager.register_tenant(
    identifier="acme-corp",
    metadata={"_enc_api_key": "sk-live-abc123", "plan": "pro"},
)
# Stored as: {"_enc_api_key": "enc::gAAAAA...", "plan": "pro"}

plain = manager.decrypt_tenant(tenant)
plain.metadata["_enc_api_key"]  # "sk-live-abc123"

L1 Cache

config = TenancyConfig(
    ...,
    cache_enabled=True,
    redis_url="redis://localhost:6379/0",
    l1_cache_max_size=1000,
    l1_cache_ttl_seconds=60,
)
# Every warm request hits the in-process LRU cache — no Redis round-trip
# Cache auto-invalidates on create / update / set_status / delete

Observability

@app.get("/metrics")
async def metrics():
    return manager.get_metrics()

# {
#   "metrics_enabled": True,
#   "l1_cache": {"size": 42, "hit_rate_pct": 94.3, "hits": 1847, "misses": 112},
#   "engine_cache_size": 7,
# }

Configuration

All fields overridable via TENANCY_* environment variables:

TENANCY_DATABASE_URL=postgresql+asyncpg://user:pass@localhost/myapp
TENANCY_ISOLATION_STRATEGY=schema
TENANCY_RESOLUTION_STRATEGY=header
TENANCY_CACHE_ENABLED=true
TENANCY_REDIS_URL=redis://localhost:6379/0
TENANCY_ENABLE_ENCRYPTION=true
TENANCY_ENCRYPTION_KEY=your-secret-key
TENANCY_ENABLE_RATE_LIMITING=true
TENANCY_RATE_LIMIT_MAX_REQUESTS=1000
TENANCY_MAX_TENANTS=500
TENANCY_ENABLE_METRICS=true

Database Compatibility

Feature PostgreSQL MySQL SQLite MSSQL
Schema isolation ✓ (native) ✓ (via DATABASE) ✓ (table prefix) ✓ (translate_map)
Database isolation ✓ (file-based) ! (manual)
RLS isolation
Hybrid isolation ✓ (partial) ✓ (partial) ! (limited)
Async driver asyncpg aiomysql aiosqlite aioodbc

Project Layout

src/fastapi_tenancy/
├── core/            # Config, context, types, exceptions
├── isolation/       # Schema, database, RLS, hybrid providers
├── resolution/      # Header, subdomain, path, JWT resolvers
├── storage/         # SQLAlchemy, in-memory, Redis stores
├── middleware/       # TenancyMiddleware (raw ASGI)
├── migrations/      # TenantMigrationManager (Alembic wrapper)
├── cache/           # TenantCache (LRU + TTL)
├── utils/           # Encryption, validation, DB compat, security
├── dependencies.py  # FastAPI dependency factories
└── manager.py       # TenancyManager — top-level orchestrator

Development

git clone https://github.com/fastapi-extensions/fastapi-tenancy
cd fastapi-tenancy
uv sync --all-extras

# Unit + integration (no Docker needed)
uv run pytest -m "not e2e"

# Full suite with real databases (containers start on demand; needs Docker)
uv run pytest

# Code quality
uv run ruff check src tests && uv run ruff format src tests
uv run mypy src
uv run bandit -r src -ll -ii

Contributing

See CONTRIBUTING.md.

Changelog

See CHANGELOG.md.

License

MIT © fastapi-tenancy contributors

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastapi_tenancy-0.5.0.tar.gz (592.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_tenancy-0.5.0-py3-none-any.whl (142.5 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_tenancy-0.5.0.tar.gz.

File metadata

  • Download URL: fastapi_tenancy-0.5.0.tar.gz
  • Upload date:
  • Size: 592.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for fastapi_tenancy-0.5.0.tar.gz
Algorithm Hash digest
SHA256 b7bf67b035d31ce145af02c17205f9c01e7251c523bf58641bfed3d9b381ff05
MD5 cf5d8817f26f1650863b6b8e7ced6e9f
BLAKE2b-256 e0d61b34a6bf7ada938be4d7c40b1fc0152fcc9423c08094bd29b1daeec6e200

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastapi_tenancy-0.5.0.tar.gz:

Publisher: release.yml on fastapi-extensions/fastapi-tenancy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fastapi_tenancy-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: fastapi_tenancy-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 142.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for fastapi_tenancy-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a8dce6ff49e5ab517fb2e1b52b323aca8835e281f5be3fc07619c3ca45033a36
MD5 c980fc500c5fb42c59496b310935af96
BLAKE2b-256 1237b2f784ad74a11be40e4dc2aa527a3190cbbd3675a232559523754c548209

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastapi_tenancy-0.5.0-py3-none-any.whl:

Publisher: release.yml on fastapi-extensions/fastapi-tenancy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page