Skip to content

Domain Types

Tenant

Tenant

Bases: BaseModel

Immutable tenant domain object.

All instances are frozen (ConfigDict(frozen=True)). To produce a modified copy use Pydantic's model_copy::

Text Only
updated = tenant.model_copy(update={"status": TenantStatus.SUSPENDED})

Attributes:

Name Type Description
id str

Opaque unique identifier (UUID or any stable string). This is the internal primary key and should be opaque to end-users.

identifier str

Human-readable slug used in URLs, headers, and subdomains (e.g. "acme-corp"). Must satisfy the tenant slug rules.

name str

Display name shown in UIs and reports.

status TenantStatus

Current TenantStatus.

isolation_strategy IsolationStrategy | None

Per-tenant override; None means use the global strategy from TenancyConfig.

metadata dict[str, Any]

Arbitrary key-value store for application-specific configuration (plan, quotas, feature flags, …).

created_at datetime

Creation timestamp in UTC.

updated_at datetime

Last-modification timestamp in UTC.

database_url str | None

Connection URL used in DATABASE isolation mode. Always masked in safe serialisation methods.

schema_name str | None

Schema name override used in SCHEMA isolation mode.

is_active

Python
is_active() -> bool

Return True when status is ACTIVE.

Source code in src/fastapi_tenancy/core/types.py
Python
def is_active(self) -> bool:
    """Return ``True`` when status is ``ACTIVE``."""
    return self.status == TenantStatus.ACTIVE

is_suspended

Python
is_suspended() -> bool

Return True when status is SUSPENDED.

Source code in src/fastapi_tenancy/core/types.py
Python
def is_suspended(self) -> bool:
    """Return ``True`` when status is ``SUSPENDED``."""
    return self.status == TenantStatus.SUSPENDED

is_deleted

Python
is_deleted() -> bool

Return True when status is DELETED.

Source code in src/fastapi_tenancy/core/types.py
Python
def is_deleted(self) -> bool:
    """Return ``True`` when status is ``DELETED``."""
    return self.status == TenantStatus.DELETED

is_provisioning

Python
is_provisioning() -> bool

Return True when status is PROVISIONING.

Source code in src/fastapi_tenancy/core/types.py
Python
def is_provisioning(self) -> bool:
    """Return ``True`` when status is ``PROVISIONING``."""
    return self.status == TenantStatus.PROVISIONING

model_dump_safe

Python
model_dump_safe() -> dict[str, Any]

Return a serialisable dict with database_url masked.

Use this method when including tenant data in logs, error responses, or audit trails to avoid leaking connection-string credentials.

Returns:

Type Description
dict[str, Any]

Plain dictionary with database_url replaced by "***"

dict[str, Any]

when the field is set.

Source code in src/fastapi_tenancy/core/types.py
Python
def model_dump_safe(self) -> dict[str, Any]:
    """Return a serialisable dict with ``database_url`` masked.

    Use this method when including tenant data in logs, error responses,
    or audit trails to avoid leaking connection-string credentials.

    Returns:
        Plain dictionary with ``database_url`` replaced by ``"***"``
        when the field is set.
    """
    data = self.model_dump()
    if data.get("database_url"):
        data["database_url"] = "***masked***"
    return data

TenantConfig

TenantConfig

Bases: BaseModel

Per-tenant quota and feature configuration.

Built from Tenant.metadata by the get_tenant_config dependency. All fields have sensible defaults so the dependency never raises even when a tenant has an empty metadata dict.

Attributes:

Name Type Description
max_users int | None

Maximum number of users allowed (None = unlimited).

max_storage_gb int | None

Maximum storage in gigabytes (None = unlimited).

features_enabled list[str]

List of feature-flag strings enabled for this tenant.

rate_limit_per_minute int

Per-tenant API rate limit.

custom_settings dict[str, Any]

Arbitrary extra configuration for application use.

AuditLog

AuditLog

Bases: BaseModel

Immutable audit-log entry for a tenant operation.

Attributes:

Name Type Description
tenant_id str

Tenant whose resource was affected.

user_id str | None

Authenticated user (None for system-initiated actions).

action str

Verb describing the operation (e.g. "create", "delete").

resource str

Resource type (e.g. "user", "order").

resource_id str | None

Identifier of the specific resource (None for collection-level actions).

metadata dict[str, Any]

Supplementary context (diff, old values, …).

ip_address str | None

Client IP address.

user_agent str | None

Client user-agent string.

timestamp datetime

Event timestamp in UTC.

TenantMetrics

TenantMetrics

Bases: BaseModel

Snapshot of a tenant's usage metrics.

Attributes:

Name Type Description
tenant_id str

Owning tenant ID.

requests_count int

Total number of HTTP requests handled.

storage_bytes int

Storage consumed in bytes.

users_count int

Number of active users.

api_calls_today int

API calls in the current calendar day (UTC).

last_activity datetime | None

Timestamp of the most recent request (None if none).

Enumerations

TenantStatus

TenantStatus

Bases: StrEnum

Lifecycle status of a tenant.

State transitions::

Text Only
PROVISIONING → ACTIVE → SUSPENDED → ACTIVE  (reinstated)
ACTIVE       → DELETED                      (soft-delete)
SUSPENDED    → DELETED                      (soft-delete)

IsolationStrategy

IsolationStrategy

Bases: StrEnum

Data-isolation strategy applied to tenant requests.

Strategies

SCHEMA: Each tenant owns a dedicated PostgreSQL/MSSQL schema. search_path is set per-connection so unqualified table references resolve to the correct schema automatically. DATABASE: Each tenant owns a separate database (or SQLite file). Strongest isolation; highest resource overhead. RLS: All tenants share the same schema and tables. PostgreSQL Row-Level Security policies enforce isolation at the engine level, with WHERE tenant_id = :id as defence-in-depth. HYBRID: Premium tenants use one strategy; standard tenants use another. Controlled via TenancyConfig.premium_tenants.

ResolutionStrategy

ResolutionStrategy

Bases: StrEnum

Method used to extract the tenant identifier from an HTTP request.

Strategies

HEADER: Read a dedicated HTTP header (default: X-Tenant-ID). SUBDOMAIN: Extract the leftmost subdomain from the Host header. PATH: Parse a fixed URL path prefix. JWT: Decode a Bearer JWT and read a configured claim. CUSTOM: Inject a user-supplied TenantResolver via TenancyManager.

Protocols

TenantResolver

TenantResolver

Bases: Protocol

Structural protocol for tenant resolution strategies.

Any object that exposes an async def resolve(request) -> Tenant method satisfies this protocol and can be used as a custom resolver without inheriting from any library class.

Example — custom cookie resolver::

Text Only
class CookieTenantResolver:
    def __init__(self, store: TenantStore) -> None:
        self._store = store

    async def resolve(self, request: Request) -> Tenant:
        slug = request.cookies.get("X-Tenant")
        if not slug:
            raise TenantResolutionError("Cookie missing", strategy="cookie")
        return await self._store.get_by_identifier(slug)

# Works without inheriting from BaseTenantResolver:
assert isinstance(CookieTenantResolver(store), TenantResolver)

resolve async

Python
resolve(request: Request) -> Tenant

Resolve the current tenant from request.

Parameters:

Name Type Description Default
request Request

A FastAPI / Starlette Request instance.

required

Returns:

Type Description
Tenant

The resolved Tenant.

Raises:

Type Description
TenantResolutionError

When the request does not carry enough information to identify a tenant.

TenantNotFoundError

When the identifier matches no known tenant.

Source code in src/fastapi_tenancy/core/types.py
Python
async def resolve(self, request: Request) -> Tenant:
    """Resolve the current tenant from *request*.

    Args:
        request: A FastAPI / Starlette ``Request`` instance.

    Returns:
        The resolved ``Tenant``.

    Raises:
        TenantResolutionError: When the request does not carry enough
            information to identify a tenant.
        TenantNotFoundError: When the identifier matches no known tenant.
    """
    ...  # pragma: no cover

IsolationProvider

IsolationProvider

Bases: Protocol

Structural protocol for data isolation strategies.

Any object that exposes the four required async methods satisfies this protocol and can be injected into TenancyManager without inheriting from BaseIsolationProvider.

Example — Redis keyspace isolation::

Text Only
class RedisIsolationProvider:
    async def get_session(self, tenant: Tenant): ...  # yields RedisClient
    async def apply_filters(self, query, tenant): ...
    async def initialize_tenant(self, tenant): ...
    async def destroy_tenant(self, tenant, **kw): ...

get_session

Python
get_session(tenant: Tenant) -> AsyncIterator[Any]

Yield a session scoped to tenant's namespace.

Source code in src/fastapi_tenancy/core/types.py
Python
def get_session(self, tenant: Tenant) -> AsyncIterator[Any]:
    """Yield a session scoped to *tenant*'s namespace."""
    ...  # pragma: no cover

apply_filters async

Python
apply_filters(query: SelectT, tenant: Tenant) -> SelectT

Return query filtered to only expose tenant's data.

Source code in src/fastapi_tenancy/core/types.py
Python
async def apply_filters(self, query: SelectT, tenant: Tenant) -> SelectT:
    """Return *query* filtered to only expose *tenant*'s data."""
    ...  # pragma: no cover

initialize_tenant async

Python
initialize_tenant(tenant: Tenant) -> None

Provision database structures for a newly created tenant.

Source code in src/fastapi_tenancy/core/types.py
Python
async def initialize_tenant(self, tenant: Tenant) -> None:
    """Provision database structures for a newly created *tenant*."""
    ...  # pragma: no cover

destroy_tenant async

Python
destroy_tenant(tenant: Tenant, **kwargs: Any) -> None

Deprovision and permanently delete all data for tenant.

Source code in src/fastapi_tenancy/core/types.py
Python
async def destroy_tenant(self, tenant: Tenant, **kwargs: Any) -> None:
    """Deprovision and permanently delete all data for *tenant*."""
    ...  # pragma: no cover