ConceptsTenancy

Tenancy

Flightdeck is multi-tenant from the schema up. Two founding rules — fail-closed tenancy (tenant resolution never defaults and never trusts request parameters) and credential-carries-ownership (the credential, not the caller, names the tenant) — govern every tenant-owned table and every tenant-facing route.

Fail-closed tenancy

Every tenant-owned table carries tenant_ref TEXT NOT NULLno default. A request’s tenant is resolved from its credential or token claim, never from a request parameter and never defaulted. There is no “default tenant” anywhere in the system. Cross-tenant reads use an explicit, reviewed allTenants marker and are expected to be rare.

The branded TenantRef type (from @dscodotco/contracts) is enforcement support — a compile-time nudge — not the safety mechanism itself. The safety is the NOT NULL, no-default column plus the credential resolution path.

Credential-carries-ownership

Tenant-facing auth follows one pattern everywhere: API keys are resource-bound rows, hashed at rest, issued against a closed scope allowlist. A key’s scope says “some tenant can do X”; the key row says which tenant. Every tenant-facing route resolves ownership from the credential that made the call — never from a path or body parameter the caller supplied.

This is the same pattern the merchant API (fdk_-prefixed keys) and the storefront’s self-serve /v1/my/* routes both use — see Reference -> Tenant-facing API for the concrete routes.

The one written exception — Host-header resolution

The storefront fleet renderer — one deployment serving every hosted store — must resolve a tenant from the Host header, which is attacker-influenced request data — on its face a violation of fail-closed tenancy. This is the one written exception, and it is fail-closed:

Host header -> verified domain-mapping row -> tenant_ref

An unknown or unverified host is a hard 404 — no fallback store, no default tenant. The mapping row is created only by the domain-verification flow (POST /v1/my/domains, POST /v1/my/domains/:id/verify in the stores module). A misconfigured DNS entry must never serve one store’s checkout under another store’s domain.