AcceptedDecided
ADR 0015: Backend health advisories (the notification bell)
Operators need to know when a backend is broken or mis-serving without hunting through pages.
On this page
- Status: Accepted
- Date: 2026-05-23
- Deciders: @jseifeddine
Context#
Operators need to know when a backend is broken or mis-serving without hunting through pages. Each
poll already yields observed state: reachability (lastSeenAt), the capability snapshot and zone
inventory from ADR-0014, and zone serials. The hazard in any alerting system is noise - a bell
that cries wolf gets muted. The user's explicit constraint: this must be "logically sound and
actually beneficial rather than just noise."
Decision#
- Advisories are a pure function of current observed state, recomputed every poll from the
BackendProfileset (ADR-0014). Nothing is hand-raised; when the condition clears, the advisory disappears. The only persisted state isfirstSeenAt/lastSeenAt(for debounce + age) andacknowledgedAt. - Curated, severity-tiered rule set - actionable only:
- ERROR - backend unreachable past threshold;
/configor/serversreturns 401/403 (API off / wrong key); has Slave zones butsecondary=no(they will never AXFR); a Slave zone with emptymasters[]; replication serial drift beyond threshold between matched managed backends. - WARN - has Master zones but
primary=no(no NOTIFY on change);autosecondary=yeswith no autoprimaries configured, or autoprimaries present butautosecondary=no(capability/intent mismatch). - INFO - reserved, off by default.
- No style/opinion rules (e.g. "you should sign this zone"). Signal is protected by what we refuse to alert on.
- ERROR - backend unreachable past threshold;
- Debounce. A condition must persist ≥2 consecutive polls (or ≥ N minutes) before it counts as active/alerting - a single failed poll never rings the bell.
- Acknowledge / snooze per
(backendId, code). Acked advisories drop out of the count but stay listed; if the advisory's detail materially changes (e.g. drift grows), the ack is invalidated and it re-alerts. - Surfacing. A bell in the top bar shows the unacked active count, coloured by max severity. The dropdown groups by backend; each row carries a one-line why it matters + how to fix + a deep link to the relevant page; live-updated over the existing SSE bus. Permission-scoped - a user only sees advisories for backends they can read.
- Storage. A
backend_advisorytable upserted by(backendId, code)each poll (severity, title, detail,firstSeenAt,lastSeenAt,acknowledgedAt). Rows absent from the latest evaluation are pruned - the table self-heals.
Rationale#
Computing from current state (rather than emitting an event stream) makes the system self-clearing and impossible to leave stale. Debounce, acknowledge, and a deliberately small rule set are the three levers that keep it high-signal. Trade-off: serial-drift detection needs cross-backend serial reads on the poll cycle (extra work, bounded by group membership), and some real-but-rare problems are intentionally left out of scope to protect signal.
Alternatives considered#
- Event-log alerting (append a row when something breaks). Goes stale, needs manual resolution, and double-counts flapping conditions.
- Per-rule thresholds exposed in the UI. Deferred: ship sane fixed thresholds first, expose later if operators ask.
- Email / webhook delivery. Deferred to a follow-up; the in-app bell + SSE is the MVP.
Consequences#
- New
backend_advisorytable (dual dialect, boot migration per ADR-0011). - A
HealthEvaluatorpure module + a poll-cycle hook that upserts/prunes advisories. - A top-bar bell component fed by the SSE bus; an advisory
codevocabulary (like the audit action vocabulary) that grows by append. - Depends on ADR-0014's capability snapshot and
BackendProfile.
References#
- ADR-0014 (capability model /
BackendProfile) lib/realtime/(SSE event bus + zone-state poller),lastSeenAtreachability modellib/audit/actions.ts(precedent for an append-only code vocabulary)