Skip to content

feat: dynamic config overlay — admin-editable operational settings (D1 overrides > YAML) #72

Description

@amondnet

Context / Motivation

status.config.yml is and stays the declarative base. Config-as-code is a deliberate differentiator against Uptime Kuma's database-only configuration: it gives version history, PR review, reproducible deploys, a migration path for upptime users, and it is what the create-statusbeam scaffold produces — consistent with the package-based distribution model in ADR-0002 (docs/adr/0002-package-based-distribution.md). Monitor definitions do not move into the database.

What is missing is the operational half. Pausing a noisy monitor at 3am, muting one notification channel, or hiding a component currently means editing YAML, committing, and redeploying — the wrong loop for a live incident.

How config is loaded today

The config document is read from KV at runtime, not baked into the build, and each app funnels it through a single seam:

  • Deploy uploads the YAML to the remote KV config keypackages/cli/src/commands/deploy.ts:47-49packages/cli/src/lib/wrangler.ts:98-101. The only things fused at web build time are the user's Wrangler ids/domain, via STATUSBEAM_WRANGLER_CONFIG (deploy.ts:55-61) — not the status config.
  • Worker reads it at runtimeapps/worker/src/index.ts:29-31 calls loadConfig, defined at apps/worker/src/ingest.ts:9-16; the webhook path does the same at apps/worker/src/webhook.ts:111-112.
  • Web SSR reads KV at runtimeapps/web/src/lib/data.ts:87-106 (getConfig, the YAML behind locale and page name) and :63-71 (getSummary, the snapshot).
  • Both apps already bind the same D1apps/worker/wrangler.jsonc:31-45 and apps/web/wrangler.jsonc:22-35 bind DB to the same database, so an overrides table is readable from either side with no new infrastructure.

This is why the overlay is a merge at two existing call sites rather than a build or deploy-flow rework.

Proposed scope (v1 overlay — operational settings only)

Editable from the admin panel (#63), stored in D1, cached in KV:

  • Monitor pause / resume — suspend checking without removing the monitor.
  • Notification mute / unmute per channel — silence one channel without touching the others.
  • Display toggles — e.g. hide a component from the public page.

Minimal touch points:

  • An overrides table in apps/worker/schema.sql (it currently defines only checks, incidents, and incident_updates).
  • A shared loadEffectiveConfig merge helper implementing overrides > YAML, called by the Worker's loadConfig and the web's getConfig so both observe the same effective config.
  • A config cache tag + purge. apps/web/src/lib/page.ts:23-26 currently sets s-maxage=60 and tags the response with status-page plus each site slug only — nothing tagged for config — so a config-only change would otherwise wait out the 60s TTL instead of purging immediately.
  • The admin UI must surface drift — show which keys are overridden versus the file baseline, and let an operator clear an override back to baseline.

Propagation nuance to design around

The two override classes reach the public page by different routes:

  • theme / name overrides apply immediately, because the web reads the config document directly in getConfig.
  • Site- and check-level overrides (e.g. pause) reach the web indirectly — the page renders from the KV summary snapshot (data.ts:63-71), so they only become visible after the next cron run rewrites that snapshot.

ADR-0004 should state this explicitly; it is the difference between "the override is live" and "the override is visible".

Tasks

  • Author ADR-0004 "Config layering: declarative YAML base + D1 runtime overlay" before implementation — precedence rule, which key ranges are overlayable, the drift/reconciliation story, and the propagation nuance above.
  • Add the overrides table and the loadEffectiveConfig merge helper; wire it into both seams.
  • Add the config cache tag and purge path.
  • Admin panel UI for the overlayable settings, including drift display.

Non-goals

  • Editing monitor definitions or URLs from the admin panel. Worker probe targets stay file-defined in v1 — an admin-editable probe URL is an SSRF surface, since the Worker would fetch an attacker-supplied target from inside Cloudflare's network.
  • Secrets stored in D1.
  • Full database-resident configuration.

Acceptance criteria

  • ADR-0004 is merged before implementation lands.
  • A monitor paused from the admin panel stops being checked without a redeploy, and the Worker and web app agree on the effective config.
  • Muting a notification channel suppresses only that channel.
  • A theme/name override is visible on the page without waiting out the 60s TTL (cache purged by tag).
  • The admin panel shows which settings are overridden versus the YAML baseline, and an override can be cleared back to baseline.
  • With an empty overrides table, behaviour is byte-for-byte identical to today's YAML-only path.

Priority / Effort / Dependencies

  • Priority: p2
  • Effort: M (1–3 days) — reduced from an initial L estimate: config is already a runtime KV read behind two single-function seams, both apps already bind the same D1, so the work is an overrides table, one merge helper, a cache tag, and the admin UI — no build-time or deploy-flow rework.
  • Dependencies: Blocked by feat: incident admin panel (v1) + MCP agent surface #63 (the admin panel is the editing surface). Related to feat: scheduled maintenance windows #65 — maintenance windows should ride the same overlay storage and precedence rather than inventing a second runtime-state mechanism.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort:MEffort: 1-3 daysp2Priority 2 - Mediumtype:featureNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions