You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 key — packages/cli/src/commands/deploy.ts:47-49 → packages/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 runtime — apps/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 runtime — apps/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 D1 — apps/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.
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.
Context / Motivation
status.config.ymlis 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 thecreate-statusbeamscaffold 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:
configkey —packages/cli/src/commands/deploy.ts:47-49→packages/cli/src/lib/wrangler.ts:98-101. The only things fused at web build time are the user's Wrangler ids/domain, viaSTATUSBEAM_WRANGLER_CONFIG(deploy.ts:55-61) — not the status config.apps/worker/src/index.ts:29-31callsloadConfig, defined atapps/worker/src/ingest.ts:9-16; the webhook path does the same atapps/worker/src/webhook.ts:111-112.apps/web/src/lib/data.ts:87-106(getConfig, the YAML behind locale and page name) and:63-71(getSummary, the snapshot).apps/worker/wrangler.jsonc:31-45andapps/web/wrangler.jsonc:22-35bindDBto 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:
Minimal touch points:
apps/worker/schema.sql(it currently defines onlychecks,incidents, andincident_updates).loadEffectiveConfigmerge helper implementing overrides > YAML, called by the Worker'sloadConfigand the web'sgetConfigso both observe the same effective config.apps/web/src/lib/page.ts:23-26currently setss-maxage=60and tags the response withstatus-pageplus each site slug only — nothing tagged for config — so a config-only change would otherwise wait out the 60s TTL instead of purging immediately.Propagation nuance to design around
The two override classes reach the public page by different routes:
theme/nameoverrides apply immediately, because the web reads the config document directly ingetConfig.summarysnapshot (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
loadEffectiveConfigmerge helper; wire it into both seams.Non-goals
Acceptance criteria
theme/nameoverride is visible on the page without waiting out the 60s TTL (cache purged by tag).Priority / Effort / Dependencies