Skip to content

Tenancy

SemitraTenancy is the tenant-resolution and tenant-scoping subsystem.

Use it when the same application needs to behave differently per tenant.

Semitra tenancy provides:

  • request-based tenant resolution
  • default tenant selection
  • tenant-aware database binding selection
  • cache namespace prefixing
  • storage key prefixing

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()
)
}
};

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.

  • 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.com to the acme tenant
  • accepting x-semitra-tenant: acme for API clients
  • mapping the public tenant 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.

  • multi-tenant APIs
  • per-customer isolation
  • tenant-specific feature flags or storage prefixes
  • request-time routing to a tenant-specific D1 binding

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.