Files
smirk-backend-core/migrations/20260718000001_operator_settings.sql
jwinterm 00968a9c05 feat(config): operator settings overlay foundation (storage + validated merge)
Stage 1 of the operator-editable config layer (additive; no behavior change yet):
- migration operator_settings: one MAC(ADMIN_KEY_INTEGRITY_SECRET) JSONB row per
  section, FK to admin_keys, audited on write. Missing section = env-only.
- OperatorSettingRow model.
- SettingsOverlay + section overlays (landing/retention/restore) with secrets
  EXCLUDED BY CONSTRUCTION; Config::apply_overlay merges onto the env-derived Config
  then re-runs the SAME Config::validate boot uses (a bad edit yields the identical
  error, nothing persists, the instance cannot be bricked).
- runtime_class() single source of truth (RuntimeSafe hot-swap vs RestartRequired).
- operator_settings storage: canonical-JSON MAC (stable across JSONB key reorder),
  load_settings_overlay (fail-closed per section -> env fallback), put_setting_audited
  (upsert + admin_audit append in one tx).
- sqlx json feature.

cargo check clean; 4 new unit tests pass. Next stages: ArcSwap AppState + ~59
read-site migration, GET/PUT /admin/config (+ restart_apply_mode), remaining sections.
2026-07-18 04:33:20 -04:00

16 lines
899 B
SQL

-- Operator-editable settings overlay: the DB precedence tier UNDER the env-derived
-- Config (effective = defaults -> env -> DB overlay). One JSONB row per section; the
-- effective Config is rebuilt from these at boot and on change. Each row is MAC'd with
-- ADMIN_KEY_INTEGRITY_SECRET (same anchor as server_config / admin_keys) so a DB-write
-- attacker cannot silently flip operator policy, and every write is appended to the
-- hash-chained admin_audit log. A missing section = env-only (no seed rows). Secrets are
-- NEVER stored here (overlays carry no secret fields by construction).
CREATE TABLE operator_settings (
section TEXT PRIMARY KEY,
doc JSONB NOT NULL,
version BIGINT NOT NULL DEFAULT 1,
updated_by UUID REFERENCES admin_keys(id) ON DELETE SET NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
integrity_mac TEXT NOT NULL
);