ADR-0055 — Hosting topology is a declared plane (A apex / B provider-subdomain); aliases compose¶
Status: Accepted (2026-08-30)
Depends on: ADR-0036 (hierarchy + host lens), ADR-0037 (membership), ADR-0033 (CSRF Origin==Host), ADR-0003, ADR-0005
Does not replace: ADR-0034 (reserved RLS capstone), ADR-0036, ADR-0037
Stem: stems/tenancy.md
CONOPS: docs/superpowers/specs/2026-08-30-hosting-topology-conops.md
Issues: #1657, #1656, #1655 (failure class)
Context¶
ADR-0036 named three tenant notions (schema registry, RLS fence, host tenant)
and bound current_tenant to the host GUC. ADR-0037 declared membership at
the root. Neither named how HTTP Host relates to a tenant. The only
implemented relation was B: TenantResolutionMiddleware requires Host to be
{slug}.{domain} or a canonical host, and ApexDiscoveryMiddleware bounced
apex logins onto slug hosts.
cookie_scope was then overloaded as a topology switch (#1657). That is the
wrong plane: in code it is only the bounce gate. set_cookie does not set
Domain. CyFuture-class apps (everything on www; tenant from membership) inherited
{slug}.{domain} routing and lost sessions.
Decision¶
D1 — Four planes; hosting is the fourth declared fact¶
Isolation, membership, hosting topology, and lens compose. Link-time: membership
root = RLS root = hierarchy root (ADR-0037 D5) and tenant_host.topology is
present and consistent across a domain. The runtime must not infer A vs B from
canonical_hosts: or cookie_scope:.
D2 — Declarative surface: topology: on tenant_host: (locality)¶
tenant_host:
topology: apex | provider_subdomain
domain: example.com
slug_field: slug
canonical_hosts: [www.example.com]
cookie_scope: host
topology: is required when tenant_host: is declared (T1, validator-owned).
It is domain-level: 2+ kinds sharing domain: must agree (Rule 6, same class
as cookie_scope / canonical_hosts / super_admin_role).
Not a top-level hosting: / tenancy: topology: block — ADR-0037 D3 and
ADR-0036 rejected second places to declare what already lives on tenant_host:.
D3 — Named topologies; leftover tokens stay put; aliases are not a token¶
| Token | Topology | Host names tenant? |
|---|---|---|
apex |
A | No. Canonical hosts only. |
provider_subdomain |
B | Yes. {slug}.{domain}. |
There is no custom_alias token. Customer hostnames are later rows in
tenant_host_aliases, composing with the app's A or B.
Unknown parse tokens (zzz, custom_alias): parse error. Missing key: T1.
Runtime leftover: mapper None / no slug extract — do not invent B.
D4 — cookie_scope is not topology¶
Default cookie_scope: host. Apex slug-bounce fires only when
topology == provider_subdomain AND cookie_scope == apex. That bounce is
not session sharing until Domain cookies are wired (follow-on). A +
cookie_scope: apex is a link-time error (T4).
D5 — Host never writes the fence except proven-token paths¶
TenantResolutionMiddleware sets _current_host_tenant_id only.
_current_tenant_id is set by the auth dependency from partition_root_id,
JWT bind, or HMAC signing lookup. Signing-on-A uses a SECURITY DEFINER
function owned by dazzle_bypass (FORCE RLS fences dazzle_owner); request
LOGIN stays dazzle_app. Do not mint tenant into the HMAC token.
D6 — Client-supplied tenant is not a resolver¶
Production resolvers: Host (B), verified alias table (later), membership (A).
Forbidden: ?tenant_id=, JSON body tenant, X-Tenant-ID as authority.
D7 — Path-prefix tenancy is forbidden¶
app.com/acme/... is not a topology.
Consequences¶
TenantHostSpec.topology: Literal["apex", "provider_subdomain"] | None- Parser closed tokens; T1 owns missing; runtime mappers take
str - A requires non-empty
canonical_hosts; unknown Host on A is 400 (no slug parse) _TenantStateMarkercarriestopology,cookie_scope,domain- Clean break: every in-tree
tenant_host:declarestopology: - Downstream: CyFuture
topology: apex; slug-host appstopology: provider_subdomain
Failure-modes check¶
- Which failure mode? Authority leak (wrong tenant fence from Host) and silent under-grant (A bounced off its session).
- Detector? RLS fence,
dazzle validate(T1–T4),resolve_apex_redirecttests keyed by topology, leftover-honesty case. - Live? Validate every build; RLS every request.
- Traceable to DSL?
tenant_host.topology+cookie_scope+canonical_hosts. - Postgres/auth/RLS preserved? Two tenant-id ContextVars stay separate.