Resolution Strategies¶
Base¶
BaseTenantResolver ¶
BaseTenantResolver(store: TenantStore[Tenant])
Bases: ABC
Optional abstract base class for tenant resolution strategies.
Subclass this to build a custom resolution strategy::
class CookieResolver(BaseTenantResolver):
async def resolve(self, request: Request) -> Tenant:
tenant_id = request.cookies.get("X-Tenant")
if not tenant_id:
raise TenantResolutionError("Cookie missing", strategy="cookie")
return await self.store.get_by_identifier(tenant_id)
Alternatively, implement the TenantResolver protocol directly —
duck-typing is sufficient (no inheritance required).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
TenantStore[Tenant]
|
The tenant metadata store used to look up tenants. |
required |
Source code in src/fastapi_tenancy/resolution/base.py
resolve
abstractmethod
async
¶
resolve(request: Request) -> Tenant
Resolve the current tenant from request.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
Incoming FastAPI / Starlette request. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
The resolved :class: |
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/resolution/base.py
Header¶
HeaderTenantResolver ¶
HeaderTenantResolver(
store: TenantStore[Tenant],
header_name: str = "X-Tenant-ID",
)
Bases: BaseTenantResolver
Resolve the current tenant from an HTTP request header.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
TenantStore[Tenant]
|
Tenant metadata store. |
required |
header_name
|
str
|
Header to read (default: |
'X-Tenant-ID'
|
Example::
resolver = HeaderTenantResolver(store, header_name="X-Tenant-ID")
# Request: GET /api/users HTTP/1.1
# X-Tenant-ID: acme-corp
tenant = await resolver.resolve(request)
# → Tenant(identifier="acme-corp", …)
Source code in src/fastapi_tenancy/resolution/header.py
resolve
async
¶
resolve(request: Request) -> Tenant
Resolve the tenant from the X-Tenant-ID header (or configured name).
All failure modes (missing header, invalid identifier format, unknown
tenant) raise :exc:TenantResolutionError with the same generic reason
to prevent tenant enumeration by callers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
Incoming HTTP request. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Resolved |
Tenant
|
class: |
Raises:
| Type | Description |
|---|---|
TenantResolutionError
|
When the header is absent, fails identifier validation, or no matching tenant exists. |
Source code in src/fastapi_tenancy/resolution/header.py
Subdomain¶
SubdomainTenantResolver ¶
SubdomainTenantResolver(
store: TenantStore[Tenant],
domain_suffix: str = "",
trust_x_forwarded: bool = True,
)
Bases: BaseTenantResolver
Resolve the current tenant from the leftmost subdomain.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
TenantStore[Tenant]
|
Tenant metadata store. |
required |
domain_suffix
|
str
|
The base domain (e.g. |
''
|
trust_x_forwarded
|
bool
|
Whether to read |
True
|
Example::
resolver = SubdomainTenantResolver(store, domain_suffix=".example.com")
# Request: Host: acme-corp.example.com
tenant = await resolver.resolve(request)
# → Tenant(identifier="acme-corp", …)
Source code in src/fastapi_tenancy/resolution/subdomain.py
resolve
async
¶
resolve(request: Request) -> Tenant
Extract the tenant identifier from the request's hostname.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
Incoming HTTP request. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Resolved |
Tenant
|
class: |
Raises:
| Type | Description |
|---|---|
TenantResolutionError
|
When the subdomain is absent, does not match the configured suffix, or fails validation. |
TenantNotFoundError
|
When the identifier has no matching tenant. |
Source code in src/fastapi_tenancy/resolution/subdomain.py
Path¶
PathTenantResolver ¶
PathTenantResolver(
store: TenantStore[Tenant],
path_prefix: str = "/tenants",
)
Bases: BaseTenantResolver
Resolve the current tenant from the URL path prefix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
TenantStore[Tenant]
|
Tenant metadata store. |
required |
path_prefix
|
str
|
The fixed path prefix before the tenant identifier
(default: |
'/tenants'
|
Example::
resolver = PathTenantResolver(store, path_prefix="/tenants")
# Request: GET /tenants/acme-corp/orders
tenant = await resolver.resolve(request)
# → Tenant(identifier="acme-corp", …)
# request.state.tenant_path_remainder == "/orders"
Source code in src/fastapi_tenancy/resolution/path.py
resolve
async
¶
resolve(request: Request) -> Tenant
Extract the tenant identifier from the request path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
Incoming HTTP request. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Resolved |
Tenant
|
class: |
Raises:
| Type | Description |
|---|---|
TenantResolutionError
|
When the path does not match the expected format or the identifier fails validation. |
TenantNotFoundError
|
When the identifier has no matching tenant. |
Source code in src/fastapi_tenancy/resolution/path.py
JWT¶
JWTTenantResolver ¶
JWTTenantResolver(
store: TenantStore[Tenant],
secret: str,
algorithm: str = "HS256",
tenant_claim: str = "tenant_id",
audience: str | None = None,
)
Bases: BaseTenantResolver
Resolve the current tenant from a signed Bearer JWT.
Reads Authorization: Bearer <token> from the request, verifies the
signature, and extracts the configured claim (default: tenant_id).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
TenantStore[Tenant]
|
Tenant metadata store. |
required |
secret
|
str
|
JWT signing secret (HMAC) or public key (RSA). Required. |
required |
algorithm
|
str
|
Signing algorithm (default: |
'HS256'
|
tenant_claim
|
str
|
JWT payload claim holding the tenant identifier
(default: |
'tenant_id'
|
audience
|
str | None
|
Expected |
None
|
Raises:
| Type | Description |
|---|---|
ImportError
|
When |
Example::
resolver = JWTTenantResolver(
store,
secret="my-super-secret-key-at-least-32-chars",
tenant_claim="tenant_id",
audience="my-api-service",
)
# Request: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...
tenant = await resolver.resolve(request)
Source code in src/fastapi_tenancy/resolution/jwt.py
resolve
async
¶
resolve(request: Request) -> Tenant
Decode the Bearer JWT and resolve the tenant from the payload claim.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
Incoming HTTP request. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Resolved |
Tenant
|
class: |
Raises:
| Type | Description |
|---|---|
TenantResolutionError
|
When the |
TenantNotFoundError
|
When the extracted identifier has no matching tenant. |