# `api-host` — Foundation gap 5: configurable default tenant **Package**: `@suaveplan/api-host` **Status**: PROPOSED **Type**: behavior-change ## Why `packages/api-stack/api/api-host/src/request-pipeline/request-pipeline.ts` hard-coded a `{id: "public" as TenantId, slug: "public"}` fallback whenever the configured `TenantResolverChain` returned `null` on an auth-optional route. The `"public"` literal became load-bearing — every handler that introspected `ctx.tenantId` had to treat the string `"public"` as a sentinel rather than receiving an honest absence. This blocked deployments that: - Genuinely run multi-tenant traffic where there is no public tenant row in the database, and want resolver miss → `400` not silent fallback. - Run an actual tenant with `id="public"` and need to distinguish "the literal public tenant" from "no tenant resolved". - Want to defer the policy choice (`null` vs. stub vs. error) to the operator's bootstrap module rather than the framework. ## What changes ADDED: `HostConfig.defaultTenant?: TenantContext | null` and `RequestPipelineOptions.defaultTenant?: TenantContext | null`. MODIFIED: §6 step 1 ("Tenant resolve") of the request pipeline. - When the resolver chain returns `null` AND the route's `auth` policy is `undefined`/`null` (auth-optional): - `defaultTenant: undefined` (the new default) → `ctx.tenant === null`, `ctx.tenantId === null`. - `defaultTenant: null` (explicit) → identical to `undefined`. - `defaultTenant: TenantContext` → inject verbatim. This preserves pre-foundation-gap-5 behaviour for migrators by setting `{ id: "public", slug: "public" }`. - When the route's `auth` is required AND the resolver chain misses, the pipeline still raises `HOST.TENANT_NOT_RESOLVED` (400) regardless of `defaultTenant` — the fallback is a convenience for public routes, not a silent bypass of tenant-required policy. Downstream pipeline steps now tolerate `tenantCtx === null`: - Rate-limit key falls back to `"anon"` when no principal AND no tenant is bound. - Per-request logger meta carries `tenantId: null`. - `succeed()` and `fail()` propagate `tenantId: null` into `PipelineResult.tenantId`. - `PipelineCapabilityResolver.forRequest` accepts `tenant: TenantContext | null`. `RequestContext.tenant` / `RequestContext.tenantId` remain declared as non-nullable in `@suaveplan/api-types`; handlers consuming tenant-bound capabilities MUST gate on `tenant === null` first when their host is configured with `defaultTenant: null`. The contract is documented in `host.md` and `request-pipeline.types.ts`. ## Migration Operators relying on the legacy `"public"` stub MUST set `defaultTenant: { id: "public", slug: "public" }` in `createHost(...)`. The E2E foundation-stack server has been migrated this way to keep its existing `tenantId === "public"` assertion green; new deployments SHOULD leave the field unset.