This framework uses a soft-tenant isolation model by default: tenant_id is a column on tenant-scoped tables, and all queries are filtered by this value. Consumers can later adopt schema-per-tenant or DB-per-tenant strategies; the API surfaces remain compatible.
How tenant is resolved
resolve_tenant_id(request)looks up tenant id in this order:- Global override hook (set via
add_tenancy(app, resolver=...)) - Auth identity (user.tenant_id or api_key.tenant_id) when auth is enabled
X-Tenant-Idrequest headerrequest.state.tenant_id
- Global override hook (set via
Use TenantId dependency to require it in routes, and OptionalTenantId to access it if present.
Enforcement in data layer
- Wrap services with
TenantSqlServiceto automatically:- Apply
WHERE model.tenant_id == <tenant>on list/get/update/delete/search/count. - Inject
tenant_idupon create when the model has the tenant field.
- Apply
Tenant-aware CRUD router
- When defining a
SqlResource, settenant_field="tenant_id"to mount a tenant-aware CRUD router. All endpoints will requireTenantIdand enforce scoping.
Per-tenant rate limits / quotas
- Global middleware and per-route dependency support tenant-aware policies:
scope_by_tenant=Trueputs requests in independent buckets per tenant.limit_resolver(request, tenant_id)lets you return dynamic limits (e.g., plan-based quotas).
Export a tenant’s data (SQL)
- CLI command:
sql export-tenant- Example:
python -m svc_infra.cli sql export-tenant items --tenant-id t1 --output out.json
- Flags:
--tenant-id(required),--tenant-field(defaulttenant_id),--limit(optional),--database-url(or setSQL_URL).
- Example:
Migration to other isolation strategies
- Schema-per-tenant or DB-per-tenant can be layered by adapting the session factory or repository to select the schema/DB based on
tenant_id. Your application code that relies onTenantIdand tenant-aware services/routers remains the same.