The delivery runtime driver: one pending delivery row → signed HTTP
send → reconciled ledger row. Pure functions over the consumer's
resource modules and an injected HTTP adapter — Oban-free by
construction; use AshHooks.Worker wraps this as an Oban worker inside
the consuming app (ADR-0004's host-injection boundary).
The delivery ROW is the single retry-policy source (ADR-0008): the
driver refuses to re-send :succeeded/:dead_letter rows, waits on
next_attempt_at, counts attempts against the ceiling, and honors
Retry-After (bounded). Oban is the durable TRIGGER — {:snooze, s}
re-drives later without exhausting the job (snooze extends
max_attempts; only this module's ceiling decides dead-letter).
Classification (the design note's table, one source):
- 2xx →
:succeeded(status + allowlisted content-type summary — NO body bytes by default; a per-callsnippet_capture: trueconfig persists the floor-redacted body marked[captured], ADR-0005's snippet amendment) - 408/429 → retryable,
Retry-Afterwhen present (integer seconds or HTTP-date; clamped[1, retry_after_cap]), else backoff - 410 → endpoint durably
:disabled + row:dead_letter - other 4xx →
:dead_letter(client errors do not self-heal) - 3xx →
:dead_letter(redirect_refused— never followed) - 5xx / transport error / secret-resolution failure → retryable backoff
- send-time SSRF refusal / disabled-or-gone endpoint →
:dead_letter
Backoff: min(base · 2^min(attempts, 16), max_backoff) seconds plus
:rand.uniform(delay) jitter, re-clamped — always ≥ 1 second.
Summary
Functions
Retention hook: deletes TERMINAL delivery rows (:succeeded,
:dead_letter) older than older_than, by the resource's
inserted_at (add Ash timestamps() to the resource and its
migration). Non-terminal rows are never deleted. Returns
{:ok, deleted_count}.
The package snippet floor (ADR-0005, amended 2026-08-22) — what
opt-in-captured bodies pass through before persistence: NFKC
normalization (fullwidth homoglyph markers fold), a bounded-fixpoint
decode chain (percent ×2 + JSON \u per-escape, re-run until stable — a
\u0025 escape can materialize % only after the percent layers), the
separator-tolerant marker patterns, the ≥16-char union-alphabet entropy
rule, a control-byte strip, and the 2048 cap. Un-redaction is impossible
by construction; invalid UTF-8 collapses to [binary].
Drives one delivery (args: %{"endpoint_id" => ..., "event_uuid" => ...},
string keys — Oban's JSON round-trip shape; atom keys tolerated).
The DEFAULT response-snippet summary (ADR-0005's 2026-08-22 amendment): a fixed grammar over the status and one ALLOWLISTED content-type token — never body bytes, never a body-derived digest (a hash is correlation material that explains nothing).
Functions
@spec prune( module(), keyword() ) :: {:ok, non_neg_integer()} | {:error, term()}
Retention hook: deletes TERMINAL delivery rows (:succeeded,
:dead_letter) older than older_than, by the resource's
inserted_at (add Ash timestamps() to the resource and its
migration). Non-terminal rows are never deleted. Returns
{:ok, deleted_count}.
The package snippet floor (ADR-0005, amended 2026-08-22) — what
opt-in-captured bodies pass through before persistence: NFKC
normalization (fullwidth homoglyph markers fold), a bounded-fixpoint
decode chain (percent ×2 + JSON \u per-escape, re-run until stable — a
\u0025 escape can materialize % only after the percent layers), the
separator-tolerant marker patterns, the ≥16-char union-alphabet entropy
rule, a control-byte strip, and the 2048 cap. Un-redaction is impossible
by construction; invalid UTF-8 collapses to [binary].
@spec run( map(), keyword() ) :: :ok | {:snooze, pos_integer()} | {:error, term()}
Drives one delivery (args: %{"endpoint_id" => ..., "event_uuid" => ...},
string keys — Oban's JSON round-trip shape; atom keys tolerated).
Returns :ok (terminal or attempted-to-terminal), {:snooze, seconds}
(retry later), or {:error, term} for a broken trigger (row missing →
:ok; the durable row is the record — a missing row is a completed or
reaped delivery, not a failure).
The DEFAULT response-snippet summary (ADR-0005's 2026-08-22 amendment): a fixed grammar over the status and one ALLOWLISTED content-type token — never body bytes, never a body-derived digest (a hash is correlation material that explains nothing).
"200 json token=application/json" # the type was allowlisted
"200 text token=other" # anything else collapses to otherThe status is an integer, the kind comes from a fixed vocabulary
(json | html | text | xml | binary | other), and the token is either an
exact allowlist member or the literal other — a hostile Content-Type
header cannot smuggle material into the ledger through this string.