# integration-settings ## Purpose Defines runtime-managed integration configuration: a single core-owned store of operator-set, non-secret overrides layered over the environment (`override → environment → default`), discovered and validated from each integration's declared configuration spec, applied by a boot-time overlay (changes take effect on restart), and surfaced through a generic per-integration operator settings page. Secrets and bootstrap remain environment-only; this capability never stores or displays a secret. ## Requirements ### Requirement: Runtime overrides are stored app-level and override-only The system SHALL store operator-set integration configuration overrides in a single core-owned table, `core.integration_config_overrides`, keyed by the flat configuration key name. A row SHALL exist only while an override is set; value encoding SHALL be the same string form the environment transport uses. Each row SHALL record who set it and when. No per-integration configuration tables SHALL be introduced. #### Scenario: Absent row falls through - **WHEN** no override row exists for a configuration key - **THEN** the key SHALL resolve from environment/flag/config-file, then its declared default #### Scenario: Clearing an override removes the row - **WHEN** an operator clears an override - **THEN** the row SHALL be deleted, restoring resolution from environment/default at next effect ### Requirement: Configuration resolves runtime override first Every declared integration configuration key SHALL resolve in the order: runtime override → environment/flag/config-file → declared default. The order SHALL be uniform across keys and integrations. #### Scenario: Override wins over environment - **WHEN** a key is set in the environment and an override row exists - **THEN** the effective value SHALL be the override #### Scenario: Environment wins over default - **WHEN** a key has no override row and is set in the environment - **THEN** the effective value SHALL be the environment value, not the declared default ### Requirement: Overrides take effect at boot The system SHALL load overrides once at startup — after migrations run, before any route or worker registration — and apply them ahead of all integration configuration reads. Saving an override SHALL NOT alter the running process's effective configuration. The settings surface SHALL state that changes apply on restart and SHALL indicate per key when a stored override differs from the boot-effective value. #### Scenario: Save does not change the running process - **WHEN** an operator saves an override while the system is running - **THEN** behavior governed by that key SHALL be unchanged until restart - **AND** the settings surface SHALL mark the key as pending restart #### Scenario: Restart applies the override - **WHEN** the system restarts with an override row present - **THEN** the override SHALL be the effective value everywhere the key is read ### Requirement: Secret keys are never runtime-managed Keys declared secret SHALL NOT be storable as overrides, SHALL NOT be editable from the settings surface, and their values SHALL never be displayed. The settings surface SHALL list secret keys by name with an environment-managed marker only. #### Scenario: Secret key rendering - **WHEN** the settings page renders an integration whose spec declares a secret key - **THEN** the key SHALL appear with its usage text and an environment-managed marker, with no value and no input #### Scenario: Secret override write is rejected - **WHEN** a write names a secret key as an override target - **THEN** the system SHALL reject it and store nothing ### Requirement: Override values are validated and typed Override writes SHALL be validated at save time against the key's declaration: values for keys declaring an enumeration SHALL be members of it. At boot, the overlay SHALL coerce each stored value by the key's declared default type (list keys comma-split, boolean and integer keys parsed) so typed reads behave identically to environment-sourced values. A stored value that fails coercion or enumeration at boot SHALL fail startup with an error naming the key and the remediation. #### Scenario: List-typed override reads as a list - **WHEN** a key declared with a list default has override value "a.example,b.example" - **THEN** typed reads of that key SHALL return two elements after restart #### Scenario: Invalid enum value rejected at save - **WHEN** an operator submits a value outside a key's declared enumeration - **THEN** the save SHALL be rejected and no row written #### Scenario: Corrupt stored value fails boot actionably - **WHEN** the overrides table contains a value that fails coercion for its key (e.g. written by hand) - **THEN** startup SHALL fail with an error naming the key and how to delete the offending row ### Requirement: Bootstrap remains environment-only Boot-time required-configuration validation SHALL continue to run before the database is available and SHALL be satisfiable from the environment alone. Overrides SHALL layer on top of a valid environment configuration and SHALL NOT substitute for required keys at validation time. #### Scenario: Required key present only as an override - **WHEN** a required key is unset in the environment but has an override row - **THEN** boot validation SHALL fail exactly as if the key were unset ### Requirement: Operator settings surface is generic and declaration-driven The operator panel SHALL provide a per-integration settings page at `/operator/integrations/{integrationKey}/settings`, rendered by one core handler and template for all integrations from the integration's declared configuration spec. For each non-secret key it SHALL show the key, its usage text, the effective value, and the winning source (override, environment, or default), with an input to set or clear the override; keys declaring an enumeration SHALL render as a closed select. Writes SHALL follow the operator panel's HTMX + CSRF-header pattern and require the operator role. Registering an integration with declared configuration SHALL surface its settings page with no per-integration template or handler edits. Override rows whose key matches no installed integration's declaration SHALL be listed with a removal affordance. #### Scenario: Settings page renders from declarations - **WHEN** an operator opens an integration's settings page - **THEN** every declared non-secret key SHALL appear with usage text, effective value, and source, and every secret key with an environment-managed marker #### Scenario: New integration needs no surface work - **WHEN** a new integration declaring configuration is registered - **THEN** its settings page SHALL be reachable and complete with no operator template or handler edits #### Scenario: Setting and clearing an override - **WHEN** an operator saves a valid value for a non-secret key and later clears it - **THEN** the save SHALL upsert the override row and the clear SHALL delete it, each reflected on the page #### Scenario: Orphan overrides are visible - **WHEN** an override row's key matches no installed integration's declaration - **THEN** the operator surface SHALL list it as unrecognized with an affordance to delete it ### Requirement: Integration status presentation distinguishes registry lifecycle from configuration readiness Operator surfaces that present per-integration state (the integrations list and the overview's system region) SHALL label the registry lifecycle value for what it is — an operator-managed registry status, defaulting to active and never changed by any health check — and SHALL NOT present it under wording that implies connectivity or live health ("connected", "read live"). Alongside the registry status, these surfaces SHALL show a configuration-readiness signal derived from the same required-key resolution the settings pages already perform: an integration with any required key unresolved SHALL be visibly marked as not configured wherever its status is shown, not only on its own settings page. #### Scenario: Unconfigured integration is visibly unconfigured on the list - **WHEN** an operator views the integrations list while an integration's required key resolves to no value - **THEN** that integration's row shows a not-configured signal alongside its registry status - **AND** the page does not describe the integration as connected #### Scenario: Registry status is labeled as registry status - **WHEN** an operator views any surface showing integration status - **THEN** the lifecycle value is labeled so it cannot read as a health check result ### Requirement: Secret settings distinguish set from unset Settings surfaces SHALL render required secret keys with a truthful presence indicator — set (masked) versus not set — rather than one identical masked treatment for both states, so an operator can tell whether a required secret exists without consulting the environment. #### Scenario: Missing secret renders as not set - **WHEN** an operator views the settings page for an integration whose required secret is absent from the environment - **THEN** the secret's row states that no value is set - **AND** a present secret renders as set-and-masked, visually distinct from the absent state ### Requirement: Switching Stripe to live mode carries a consequence warning The `stripe-mode` setting SHALL disclose, adjacent to the control and before submission, that selecting `live` moves the deployment onto real payment processing with real charges, distinguishing this control from routine settings on the same page. #### Scenario: Live mode selection is warned before save - **WHEN** an operator selects `live` for `stripe-mode` - **THEN** consequence copy adjacent to the control states that real charges result - **AND** the copy renders before the operator submits the change