Enforce 10j's verified gaps (schema-hardening change): - Migration 00010: partial unique indexes for one default pool and one primary assignment per workspace, plus CHECKs pinning pool/provider/subscription vocabularies and provider lifecycle timestamps. - Workspace creation shares a transactional provisioning function; extension validates its target pool; last-tier deletion of a defaulted ladder is guarded; signup completes plan-less on a broken ladder. - Boot asserts integration slug parity and validates declared config enums; Stripe invoice amounts are range-checked; domain cancellation runs a final evidence probe; rule authoring is additive-only.
7.3 KiB
integration-registration Specification
Purpose
TBD - created by archiving change integration-extraction. Update Purpose after archive.
Requirements
Requirement: Single registration point
An installed integration SHALL consist of one self-contained tree under
internal/integrations/<slug>/ and exactly one entry in the integration
registry. The composition root SHALL derive the integration's migration
source, provider manifest, routes, workflow registration, startup hooks, and
configuration keys from the registry. Adding or removing an integration
SHALL NOT require edits to internal/server, internal/workflows,
internal/config, or cmd beyond the registry entry.
Scenario: Adding an integration touches only its tree and the registry
- WHEN a new integration is added to the codebase
- THEN the diff outside
internal/integrations/<slug>/SHALL be limited to the registry entry (import + slice element)
Scenario: Removing an integration
- WHEN an integration's registry entry is removed
- THEN the application SHALL build and boot without its routes, workflows, config keys, or migration stream
Requirement: Import direction is integration to core only
Integration trees MAY import core packages; core packages SHALL NOT import
integration trees. Only composition roots (cmd, internal/migrate) SHALL
import the registry. internal/server and internal/workflows SHALL
contain zero imports of integration packages.
Scenario: Core package attempts integration import
- WHEN a change adds an
internal/integrations/...import to a package outside the composition roots - THEN review (and the verification grep gate) SHALL reject it
Requirement: Capability hooks are optional
The mandatory integration surface SHALL be: slug, provider manifest, and migration source. Routes, workflow registration, startup hooks, configuration declarations, and UI assets SHALL be optional capability hooks; an integration that does not implement a hook SHALL contribute nothing to that subsystem and SHALL NOT be required to stub it.
Scenario: Integration without UI
- WHEN a registered integration implements no UI-assets hook
- THEN the server SHALL compose templates and static assets without it and boot normally
Requirement: Integration configuration is declared, not hand-wired
An integration SHALL declare its configuration keys — name, default, secret
flag (implying a *-file variant resolved at boot), required-together
groups, and optionally a closed value enumeration — through its
registration. Flag/env/default binding and boot-time validation SHALL be
driven by these declarations. The same declarations SHALL drive the runtime
settings surface: they are the sole source for which keys are
operator-manageable (non-secret keys), how values are validated
(enumeration membership) and typed (declared default type), and how keys
are presented (usage text). Integration defaults SHALL NOT be duplicated in
core configuration files; secrets SHALL remain env/file-based.
Scenario: Declared required-together group is enforced
- WHEN an integration declares two keys required together and only one is set at boot
- THEN validation SHALL fail before any service initializes, naming both keys
Scenario: Declarations are the single source for the settings surface
- WHEN an integration adds a non-secret key to its declared configuration spec
- THEN the key SHALL appear on that integration's runtime settings surface with no change outside the integration's own declaration
Requirement: Dispatch transport is chosen per provider
The extension contract SHALL describe provider capabilities (lifecycle
verbs), not a mandated transport. A provider MAY dispatch mutating verbs via
direct Temporal workflows or via the transactional outbox; the outbox
drainer for a provider SHALL be owned by that provider's tree. Core code
SHALL write core.outbox rows only through the shared enqueue helper,
never with raw SQL.
Scenario: Two providers use different transports
- WHEN one provider dispatches via Temporal and another via the outbox
- THEN both SHALL be conformant without contract exceptions
Scenario: Raw outbox write in core
- WHEN core code needs to enqueue an outbox row
- THEN it SHALL call the shared enqueue helper
Requirement: UI assets are slug-namespaced
An integration's template files and defined template names SHALL be prefixed with its slug; its static assets SHALL be served from a per-integration mount under the existing static route, same-origin. Name collisions across integrations SHALL be structurally impossible under the prefix rule.
Scenario: Template name collision
- WHEN two integrations each register templates following the slug prefix rule
- THEN their template names SHALL NOT collide
Requirement: Member dashboard contribution is an optional capability hook
An integration MAY contribute member dashboard cards by implementing a dashboard-card capability interface, declared at the point of use in the server package (matching the existing route-provider pattern and its import-direction rationale). A card declaration SHALL consist of a title, an HTMX partial path, an optional refresh event name, and zero or more page-level script paths under the integration's slug-namespaced static mount. The composition root SHALL collect declarations by type-asserting registered integrations; an integration that declares no cards SHALL require no dashboard-related code. Card body markup SHALL be delivered by the integration's own routes; core SHALL NOT render integration-owned templates inline into the dashboard page.
Scenario: Integration without member dashboard cards
- WHEN an integration does not implement the dashboard-card interface
- THEN it SHALL register and operate normally with no dashboard presence
Scenario: Declarations are collected at the composition root
- WHEN the application composes registered integrations at startup
- THEN dashboard-card declarations SHALL be collected from every integration implementing the hook and passed to the server
- AND adding a card-declaring integration SHALL touch only its own tree and the registry
Scenario: Core never renders integration templates inline
- WHEN the dashboard page renders declared cards
- THEN core SHALL emit only the generic card shell; all card body content SHALL be fetched from the integration's declared partial route
Requirement: Slug declarations agree at boot
For every registered integration, boot SHALL assert that the integration's Slug() method and the slug in its provider manifest are equal, and SHALL fail startup with an error naming the integration and both strings when they differ. This turns the documented must-match rule into an enforced one; a mismatch previously registered the provider under one slug while mounting its UI and settings under the other.
Scenario: Mismatched slugs stop the boot
- WHEN an integration's
Slug()returns a different string than its manifest's slug - THEN the application fails to start, and the error names the integration and both slug values
Scenario: Agreeing slugs boot normally
- WHEN every integration's two slug declarations agree
- THEN registration proceeds exactly as before