Skip to main content

Production-ready multi-tenancy for FastAPI — schema, database, RLS, and hybrid isolation with full async support.

Project description

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
docker compose -f docker-compose.test.yml up -d
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

Project details


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.4.0.tar.gz (247.0 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.4.0-py3-none-any.whl (128.2 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for fastapi_tenancy-0.4.0.tar.gz
Algorithm Hash digest
SHA256 483b639cd89a7b6a5ee87c9d53245562f317ccc2c1dd16a1ffaffaf2eafadd5d
MD5 138e5007f88324ee7d325b744c4e7d27
BLAKE2b-256 877327d8fa49628dd97229731d922c2a0e00cd09304f97da3541041632a2d3d5

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastapi_tenancy-0.4.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.4.0-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_tenancy-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e3ebe805f4c67994a242574cb7fc0461d5715c046e941ac1829b68f9cfa3bea5
MD5 7a49180e4baabdc0c61820cd35392669
BLAKE2b-256 4c62a20f2f94a7d6f115b0247bb3409a111e74c35c4c607bc7daa8acf5813326

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastapi_tenancy-0.4.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.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page