Move member domain management affordances to point-of-use within site flows and retire the standalone member `/domains` page. Document integration interdependence exploration.
6.8 KiB
Integration interdependence — the road not taken for domains
2026-07-26. Written while de-centering domains from the console IA. This records the path we considered and deferred — registering domains as a provider and letting integrations depend on each other — so a future external factor can pick it up without re-deriving it.
Where this came from
The de-centering discussion produced two candidate homes for the operator domains surface: registry membership ("one click, visible" — domains becomes a provider row and the Integration group lists it like Discourse or FedWiki), or staying a plainly-listed core capability. We chose the second: Domains keeps its operator sidebar entry (honest — it is a cross-workspace operator capability), the member surface dissolves to point-of-use, and the provider question is parked here.
What registry membership would have looked like (mechanically cheap)
The registry would take domains today with almost no force:
core.providersstores no credentials or endpoints — slug, kind, display_name, nullableoperator_surface_path, free-text status (internal/db/migrations/00001_init.sql:864-875). An "internal" provider fits the table as-is.- Registration is code-driven and idempotent: a
ProviderSourcemanifest collected at boot, upserted byintegration.RegisterProviders(cmd/start.go:165-172,internal/integration/registration.go:123-234). Domains would ship a manifest like any integration package. - The operator sidebar's Integration group is registry-driven —
ListProvidersByKind('provisioning'), skipping rows without a surface path (internal/server/operator_pages.go:49-67). A domains row withOperatorSurfacePath: "/operator/domains"appears with zero template work. - The one schema obstacle:
chk_providers_kind_validcloses the kind enum atpayment | provisioning | notification | tax(00001_init.sql:874, mirrored inregistration.go:18-23). Domains either reusesprovisioning(defensible: it provisions names) or the CHECK gains aninfrastructure/internalkind by migration. - Bonus repair: the manifest could own the
external_domain_claimsresource key, pulling the custom-domains entitlement gate toward core. Today that gate is enforced in fedwiki's web layer (internal/integrations/fedwiki/web/customdomain.go:37-56) — a misplacement that predates this discussion and is filed as extraction-prep debt regardless of path.
Why we didn't: the categorical problem
Every dependency arrow points into internal/domains, and the reverse
edge is explicitly forbidden (internal/domains/authorizer.go doc comment,
design D5/D6). FedWiki's create saga reserves names via the registry
(internal/integrations/fedwiki/workflows/activities.go:107-118), its sync
projects placements, and core defines the domainsReconciler hook that
integrations implement (cmd/start.go:34-48). Integrations are tenants
of the namespace; domains is the landlord. The operator-panel-navigation
spec encodes the same idea as its one-way catalog → runtime → integration
dependency rule: the Integration group is for things at the top of the
dependency order, and domains sits at the bottom.
Registering the landlord in the tenants list is mechanically free and conceptually wrong — unless interdependence becomes a first-class, designed-for property of the provider model. That design work is the real cost, and it's what this document scopes.
The design work interdependence actually needs
If integrations may depend on each other (or on provider-shaped infrastructure like domains), each of these needs an answer before the first dependent ships — retrofitting any of them is misery:
- Dependency declaration. Manifests grow
DependsOn []slug.RegisterProvidersvalidates the graph is a DAG and refuses cycles at boot, loudly. Ordering of registration/boot hooks derives from the topological sort, not package-import accident. - Enablement vs health.
statustoday is one free-text field. Interdependence forces the split: a provider can be healthy but disabled, enabled but degraded, and — the new case — enabled but starved because a dependency is disabled. Disabling a depended-on provider must either cascade (disable dependents), refuse ("FedWiki depends on Domains"), or degrade dependents explicitly. Silent starvation is the tomfoolery baseline to design against. - Mediated capability vs code import. FedWiki imports
internal/domainsdirectly. A provider-mediated dependency implies the registry (or a capability interface at that layer) fronts what dependents may call — otherwiseDependsOnis decorative metadata over ordinary imports, and the model lies. Deciding metadata-only is a legitimate outcome, but it must be decided, not drifted into. - Blast radius. Placements already accept any registered provider
slug; the write model is "integrations grant on and write
domains.*" (internal/domains/store.go:19). Interdependence needs explicit rules for what a provider may do to a dependency's resources — per-operation grants, not ambient schema access — or contraption-building follows. - UI honesty. If infrastructural providers list under Integrations, the group stops meaning "external systems" and starts meaning "registered modules." Either the group's definition changes in the IA spec, or infrastructural providers render distinctly (sub-grouped, badged). The sidebar is a claim about architecture; it should stay true.
Reopening triggers
Park until one of these applies, then start from this list:
- A second consumer of domains (anything beyond FedWiki wanting placements — forum vanity domains, static-site hosting, …).
- M13 multi-farm, which the 10d exploration already flags as landing "in the FedWiki integration, not the registry" — pressure on the boundary will make the dependency explicit.
- Actual extraction demand: someone wants the domains model as a
standalone service/package. The module's leaf-with-IoC shape is
extraction-ready; what extraction adds is a network API in front of
Registry/Tx, inter-service authn, latency tolerance in the create saga, and schema ownership — at which point domains-as-provider (kindinfrastructure, remote endpoint) becomes the natural registration and points 1–4 above become mandatory rather than speculative. - Any new integration that needs another integration — the general case arriving by a different door.
Decision record
2026-07-26 — deferred. Chosen instead: member /domains dissolves into the
site-creation/site-management flow; operator Domains keeps its sidebar
entry; overview metric departs in the overview-polish change; the
entitlement-gate misplacement is tracked as debt. No registry membership,
no DependsOn, no new provider kind.