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

Metadata

Release files for fastapi-tenancy 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fastapi-tenancy 0.5.0
File Size Uploaded
fastapi_tenancy-0.5.0.tar.gz 592.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fastapi-tenancy 0.5.0
File Interpreter ABI Platform
fastapi_tenancy-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 734.8 kB

Release files / fastapi_tenancy-0.5.0.tar.gz

Download URL fastapi_tenancy-0.5.0.tar.gz
Size 592.3 kB
Tags Source
SHA-256 checksum
How to use checksums
b7bf67b035d31ce145af02c17205f9c01e7251c523bf58641bfed3d9b381ff05
BLAKE2b-256 checksum
How to use checksums
e0d61b34a6bf7ada938be4d7c40b1fc0152fcc9423c08094bd29b1daeec6e200
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.

Transparency log

Release files / fastapi_tenancy-0.5.0-py3-none-any.whl

Download URL fastapi_tenancy-0.5.0-py3-none-any.whl
Size 142.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a8dce6ff49e5ab517fb2e1b52b323aca8835e281f5be3fc07619c3ca45033a36
BLAKE2b-256 checksum
How to use checksums
1237b2f784ad74a11be40e4dc2aa527a3190cbbd3675a232559523754c548209
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release 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