Tenancy
SemitraTenancy is the tenant-resolution and tenant-scoping subsystem.
Use it when the same application needs to behave differently per tenant.
What tenancy does
Section titled “What tenancy does”Semitra tenancy provides:
- request-based tenant resolution
- default tenant selection
- tenant-aware database binding selection
- cache namespace prefixing
- storage key prefixing
Resolution strategies
Section titled “Resolution strategies”The example app composes tenant resolvers from:
- request headers
- subdomains
That lets Semitra support both explicit tenant signaling and tenant-by-host resolution.
The reference app uses that exact pattern:
import { composeTenantResolvers, createBoundTenantDatabaseProvider, headerTenantResolver, subdomainTenantResolver} from "@semitra/cli";
export default { tenancy: { defaultTenant: "public", databaseProvider: createBoundTenantDatabaseProvider({ bindingMap: { public: { DB: "DB" } } }), resolver: composeTenantResolvers( headerTenantResolver(), subdomainTenantResolver() ) }};Database providers
Section titled “Database providers”Semitra also supports tenant database providers so you can map a tenant and database name to a concrete binding.
That is how the framework chooses the right D1 database for a request.
It also feeds cache and storage prefixing, so a request resolved for acme
keeps the same tenant identity across the full stack.
Example use cases
Section titled “Example use cases”- a SaaS app with one tenant per customer
- an admin-facing app that routes public and private tenants differently
- per-tenant storage paths and cache scopes
- shared application code that needs tenant-specific data separation
Concrete examples include:
- resolving
acme.example.comto theacmetenant - accepting
x-semitra-tenant: acmefor API clients - mapping the
publictenant to a shared D1 binding while isolating others - prefixing object keys like
tenants/acme/...automatically
Once the tenant is resolved, the rest of the stack can stay declarative:
const cache = this.runtime.adapters.cache;const storage = this.runtime.adapters.storage;
await cache.set("feature-flags", flags, { namespace: "config", scope: "tenant"});
await storage.put("exports/monthly.csv", csvBytes, { contentType: "text/csv"});With tenant scoping enabled, those writes resolve under the current tenant’s cache namespace and storage prefix without the controller manually rebuilding tenant-specific keys.
Good use cases
Section titled “Good use cases”- multi-tenant APIs
- per-customer isolation
- tenant-specific feature flags or storage prefixes
- request-time routing to a tenant-specific D1 binding
Tenancy discipline
Section titled “Tenancy discipline”Keep tenant logic in the tenancy subsystem and runtime context, not scattered across controllers and models. If you need a tenant value, resolve it once and let the rest of the framework consume it consistently.