Architecture Decision Records
One short, backward-looking document per non-obvious design decision — its context, the decision, and the consequences — so a future contributor can see why a thing is the way it is and not accidentally undo a deliberate choice. Why this format and not a monolithic design doc: 0001 .
The system in one pass
certel is a self-contained certificate monitor (0002 ): one binary, one YAML file, its own scheduler, state and alerting — no Prometheus/Alertmanager required. The ADRs each pin one link of the chain:
- Probe. On a fixed cycle the scheduler
(0014
) probes each
target — identity
protocol//address/servername(0016 ) — over implicit TLS or STARTTLS. The probe accepts legacy handshakes and verifies trust itself (0013 ), verifying the chain before diagnosing expiry (0012 ). - Decide. Statuses map onto the
ok < warning < critical < emergencyladder (0008 ); the day thresholds have exact boundary semantics (0011 ); network-shaped statuses must persistflap_streakcycles before they count (0009 ). The alertManagerkeeps one decision stream per target with a severity-aware repeat cadence (0010 ) and commits confirmed state to SQLite atomically with the deliveries it owes (0003 ). - Deliver. A decision enqueues one rendered notification per attached
notifier — fan-out is delivery-only, the repeat clock lives on the target
(0006
) — into a kind-agnostic outbox
(0004
) drained by isolated
per-notifier dispatchers. Notifiers are plain config webhooks
(0005
) with structured bodies
(0018
) and a stateless
min_severityfloor (0007 ). - Observe & contract. Optional Prometheus metrics with ssl_exporter-compatible names (0015 ); the whole config surface was reviewed and locked before 1.0 (0017 ).
Conventions
- Location & filename:
docs/adr/adr-NNNN-kebab-case-title.md, four-digit zero-padded number, assigned monotonically in order added (never renumbered). - Template: copy
adr-0000-template.md. - Status: one of
Proposed·Accepted·Superseded by NNNN·Deprecated. ADRs are append-only: a decision that changes gets a new ADR that supersedes the old one; the old one is markedSuperseded by NNNNand left in place, not edited away. - Cross-reference, don’t duplicate.
docs/alternatives.md(the build-vs-buy survey) anddocs/metrics.md(the full metrics surface) are longer living design docs; ADRs link to them rather than copying their content.
Lifecycle: how a todo/ becomes an ADR
The todo/ tree holds forward-looking work items; ADRs are backward-looking
rationale. When a todo/ design lock is resolved and its feature ships, its
outcome graduates into an ADR and the todo is deleted. The ADR must absorb
everything decision-relevant from the todo — ADRs do not cite deleted todo
files; the shipping commit’s history holds their full text if archaeology is
ever needed. Open todos (e.g. config-reload.md, notifier-failure-backoff.md,
packaging.md) are not ADRs yet; they become one only once decided and
shipped.
Not every change earns an ADR. Bug fixes and mechanical hardening — timeouts, path escaping, CI pinning, input validation — live in git history, not here. An ADR is for a decision that shaped the design and could plausibly be reversed by someone who didn’t know why.
Index
| ADR | Decision |
|---|---|
| 0001 | Record architecture decisions in ADRs |
| 0002 | Build a self-contained monitor, not an exporter add-on |
| 0003 | Persist alert state and queue deliveries in SQLite |
| 0004 | A kind-agnostic, notifier-scoped delivery queue |
| 0005 | Config-only named webhook notifiers |
| 0006 | Multi-notifier fan-out is delivery-only; the repeat clock lives on the target |
| 0007 | Per-notifier min_severity as a stateless delivery floor |
| 0008 | Three-tier severity ladder and the status→severity mapping |
| 0009 | Debounce network-shaped statuses; fact statuses alert on the first cycle |
| 0010 | Severity-aware repeat cadence |
| 0011 | Alert when strictly fewer than N whole days remain |
| 0012 | Verify the chain before diagnosing expiry |
| 0013 | Accept TLS ≥ 1.0, verify trust manually; the weak_signature status |
| 0014 | Size the scheduler cycle and liveness to the worst legal cycle |
| 0015 | Prometheus metrics surface and ssl_exporter name compatibility |
| 0016 | “target” as the domain vocabulary and Target.Key identity |
| 0017 | Lock the config surface before 1.0 |
| 0018 | Structured webhook bodies with ${namespace.path} interpolation |