Multi-Tenancy & Routed Clients
Relevant source files
The following files were used as context for generating this wiki page:
Forze provides a robust multi-tenancy system designed to isolate data and infrastructure across different organizational units or clients. This system centers on the TenantIdentity, which represents the currently active tenant in the ExecutionContext, and a suite of "Routed Clients" that derive their configuration—such as database connections or object storage buckets—dynamically based on this identity.
The architecture supports multiple isolation tiers, ranging from shared tables with tenant markers to dedicated infrastructure per tenant. By binding the tenant at the application boundary (e.g., HTTP middleware or worker interceptors), the framework ensures that adapters automatically route requests to the correct resources without requiring manual tenant ID propagation through domain logic.
Sources: skills/forze-auth-tenancy-secrets/SKILL.md, src/forze/application/contracts/tenancy/ports.py
Isolation Tiers
Forze defines a "declared floor" for isolation, allowing developers to specify the minimum required isolation level for a specific module. This prevents under-isolated wiring on sensitive query paths.
| Tier | Description |
|---|---|
| None | No isolation enforced. |
| Tagged | Shared store with a tenant marker (e.g., a SQL tenant_id column). |
| Namespace | Per-tenant container on a shared instance (e.g., dynamic SQL schema, S3 bucket prefix). |
| Dedicated | Routed client with per-tenant credentials and dedicated connections. |
Sources: skills/forze-auth-tenancy-secrets/SKILL.md
Core Components and Data Models
Tenant Domain Models
The system uses specialized models to represent tenants and their state within the identity plane.
- Tenant: The core entity representing a tenant, containing fields for status (
is_active) and a uniquetenant_key. - TenantIdentity: A lightweight value object used in execution contexts, holding the
tenant_id(UUID) and thetenant_key.
@final
@attrs.define(slots=True, kw_only=True, frozen=True)
class Tenant(Document):
tenant_key: str | None = None
is_active: bool = True
@final
@attrs.define(slots=True, kw_only=True, frozen=True)
class TenantIdentity:
tenant_id: UUID
tenant_key: str | None = None
Sources: src/forze_identity/tenancy/domain/models/tenant.py:11-23, src/forze/application/contracts/tenancy/ports.py:13-21
Tenancy Ports
Infrastructure interacts with the tenancy system through several key ports:
- TenantResolverPort: Resolves a
TenantIdentityfrom various hints (e.g., a domain name, a header, or a JWT claim). - TenantManagementPort: Handles administrative tasks such as provisioning new tenants and managing principal-to-tenant memberships.
- TenantProvisionerPort: Responsible for creating the physical infrastructure (like a Postgres schema or S3 bucket) during tenant onboarding.
Sources: src/forze/application/contracts/tenancy/ports.py:24-118
Tenancy Resolution Flow
The following sequence shows how a tenant is resolved and bound to the execution context during an incoming request.
The TenantIdentityResolver merges credential-bound tenant IDs, header hints, and results from the TenantResolverPort to establish the context.
Sources: skills/forze-auth-tenancy-secrets/SKILL.md, src/forze_identity/tenancy/adapters/resolver.py
Tenant Management and Provisioning
The TenantManagementAdapter orchestrates the lifecycle of a tenant. When a tenant is provisioned, it first creates a record in the tenant catalog and then invokes the TenantProvisionerPort to set up infrastructure.
Management Operations
The adapter provides methods to manage the relationship between principals (users/service accounts) and tenants.
| Method | Purpose |
|---|---|
provision_tenant | Creates tenant record and runs idempotent infrastructure provisioning. |
attach_principal | Binds a principal to a tenant for authorization. |
list_principal_tenants | Returns all active tenants a principal can access. |
deprovision_tenant | Tears down per-tenant infrastructure. |
Sources: src/forze_identity/tenancy/adapters/management.py:59-165
Routed Clients
Routed clients are adapters that change their behavior based on the TenantIdentity found in the ExecutionContext. For example, a RoutedPostgresClient might switch connection strings or schemas at call time.
Implementation Pattern
Tenant-aware adapters do not accept tenant IDs as arguments in their methods. Instead, they derive routing from the ambient context.
- Postgres: Supports database-per-tenant or schema-per-tenant via
PostgresSchemaTenantProvisioner. - Object Storage: Uses
ObjectStorageTenantProvisionerto manage tenant-specific buckets. - Outbound HTTP:
RoutedHttpClientresolves base URLs and credentials from per-tenant secrets.
Sources: skills/forze-auth-tenancy-secrets/SKILL.md, skills/forze-framework-usage/SKILL.md
Summary
Multi-tenancy in Forze is an infrastructure-level concern handled through context binding and routed adapters. By separating the logical TenantIdentity from the physical provisioning and routing logic, the framework allows domain code to remain "tenant-agnostic" while ensuring strict data isolation at the storage and transport layers. Key components like the TenantManagementAdapter and various Provisioner ports automate the lifecycle of these isolated environments.