Turns the consumer's resource into the outbound delivery ledger: a durable,
deduplicated obligation recording that an event must be delivered to an
endpoint (the outbound twin of AshHooks.InboundDelivery). Sends remain
at-least-once, and receivers deduplicate retries by webhook id.
use Ash.Resource,
data_layer: AshSqlite.DataLayer,
extensions: [AshHooks.OutboundDelivery]
# optional: rename the exact-bytes column when `payload` is
# reserved by the consumer's domain (e.g. a sole payload store)
outbound_delivery do
payload_attribute :event_bytes
endThe extension injects the delivery fields (event_uuid — the webhook
id, immutable across retries; the exact payload bytes to sign;
endpoint_id/subscription_id; immutable dispatch_source and
dispatch_route; the lifecycle status; accounting attempts; fenced
send and enqueue tokens/leases; the durable 410 endpoint snapshot;
the machine-written response_status/response_snippet/
next_attempt_at; bounded last_error), the unique_delivery
identity on [:endpoint_id, :event_uuid]. The ownership fields default to
explicit direct/unbound markers so existing direct callers of :dispatch
remain valid; the first authorized direct driver and worker route bind them
once with compare-and-set updates. Host declarations that replace these
fields with incompatible types, nullability, defaults, or writable machine
fields fail resource verification. Oban admission includes this
receiver identity plus the complete row key, source, route, and tenant.
The extension also injects the machine primitives :dispatch (no-touch unique upsert, the
:ingest mirror) and :mark_enqueue_failed.
Storage-level uniqueness on unique_delivery is the idempotency
primitive: the consumer's migration must carry the matching unique
index (ADR-0003's argument, applied outbound).
response_status, response_snippet (the no-body summary by default —
status + allowlisted content-type token; the [captured]-marked,
redacted body only on the runtime's per-call opt-in), and next_attempt_at
are writable?: false — no consumer create/update input accepts them;
they reach the ledger only
as arguments of the runtime's machine primitives (:mark_succeeded,
:mark_send_failed), fenced to the current attempt. Request and response
headers are not stored on this resource.
Rows contain the exact payload bytes you dispatched. Define read policies in your domain; the extension does not inject them.
Policy obligation: the package injects write actions; your application
defines their authorization.
Under a permissive base policy fragment (an authorize-if-active-member
fallback), every actor could otherwise reach the machine primitives —
mark a delivery succeeded (suppressing a real send), dead-letter one,
requeue, or prune the ledger. The injected write actions on this
resource: :dispatch, :mark_enqueue_failed, :bind_dispatch_source,
:bind_dispatch_route, :claim_enqueue, :release_enqueue, :requeue, :prune
(the retention destroy — omittable via prune_action :none),
:mark_sending, :mark_succeeded, :mark_send_failed,
:mark_disable_pending, :finalize_disable. Cover each
with an action-specific policy (the runtime's internal calls run
authorize?: false and never consult them), or ensure no actor-facing
surface (json_api/graphql/code_interface/admin) exposes the resource.
The runtime writes through these actions — a policy per action is
the complete seam.
Summary
Functions
The delivery state machine's statuses, in lifecycle order.
Functions
@spec statuses() :: [atom()]
The delivery state machine's statuses, in lifecycle order.