Webhooks for Ash Framework — inbound (receive, verify, dedup → domain events) and outbound (sign, deliver, retry, track).
Attach to a resource:
use Ash.Resource,
extensions: [AshHooks]
webhooks do
# convention-resolves to AshHooks.Provider.ComplyCube
inbound :comply_cube do
secret {:app_env, [:my_app, :complycube_secret]}
# optional: extract a stable event id (payload -> {:ok, id} | :error);
# otherwise the provider identity callback, then raw-body digest, applies
end
# convention-resolves to AshHooks.Provider.HubSpotV3 — the
# vendor-default 300s replay window applies; override it either way
inbound :hub_spot_v3 do
secret {:app_env, [:my_app, :hubspot_client_secret]}
end
outbound :order_paid do
signing_mode :dual
end
endBoth halves are independently consumable: inbound-only consumers pull no queue infrastructure (ADR-0004).
Multi-tenant? Declare Ash attribute multitenancy on the four resources
(subscription, endpoint, both ledgers) and pass :tenant /
ctx[:tenant] to every entry point — every query, write, dedup
identity, sweep, and job arg is tenant-scoped, and a tenant-less call
against a multitenant resource fails closed with
{:error, :tenant_required} before any data access. Single-tenant
apps change nothing. The
adoption checklist
walks the ordered transition (ADR-0011).
Summary
Types
The per-tenant sweep totals: one result per tenant (error-isolated — one tenant's failure never stops the others) plus the sum of the successful counts.
Functions
Fans an outbound event out to its subscribed endpoints — the outbound
entry point, delegating to AshHooks.Dispatcher.dispatch/4 (the
AshHooks.Ingress.ingest/4 twin).
Maps the retention prune over tenants: sequentially (routing by the
resource's extension: inbound ledgers through AshHooks.Ingress.prune/2,
outbound deliveries through AshHooks.Delivery.prune/2) — the same
per-tenant isolation and sequencing as reap_all/2.
Maps AshHooks.Ingress.reap/2 over tenants: SEQUENTIALLY, per-tenant
error isolation — a tenant-less global reap over a multitenant ledger is
impossible by construction (each tenant's call carries its own
pre-flight). Sequential by design: the shared repo pool is the one
cross-tenant starvation surface, so per-tenant sweeps do not contend.
Recovers eligible outbound rows for a declaration and tenant, including
interrupted enqueue, due retries, and expired send leases. Schedule this
periodically for each outbound declaration and tenant; it preserves retry
timing and attempt counts. See AshHooks.Dispatcher.reconcile_pending/3.
Schema validator for the secret option — accepts only secret SOURCES,
rejecting a literal binary at DSL parse time (ADR-0005).
Types
@type sweep_totals() :: %{ results: %{optional(term()) => {:ok, non_neg_integer()} | {:error, term()}}, total: non_neg_integer() }
The per-tenant sweep totals: one result per tenant (error-isolated — one tenant's failure never stops the others) plus the sum of the successful counts.
Functions
@spec dispatch(module(), atom(), AshHooks.Event.t() | term(), keyword()) :: {:ok, [AshHooks.Dispatcher.dispatch_result()]} | {:error, term()}
Fans an outbound event out to its subscribed endpoints — the outbound
entry point, delegating to AshHooks.Dispatcher.dispatch/4 (the
AshHooks.Ingress.ingest/4 twin).
@spec prune_all(module(), keyword()) :: {:ok, sweep_totals()} | {:error, term()}
Maps the retention prune over tenants: sequentially (routing by the
resource's extension: inbound ledgers through AshHooks.Ingress.prune/2,
outbound deliveries through AshHooks.Delivery.prune/2) — the same
per-tenant isolation and sequencing as reap_all/2.
{:ok, %{results: %{"org_a" => {:ok, 2}}, total: 2}} =
AshHooks.prune_all(Delivery, older_than: cutoff, tenants: ["org_a"])
@spec reap_all(module(), keyword()) :: {:ok, sweep_totals()}
Maps AshHooks.Ingress.reap/2 over tenants: SEQUENTIALLY, per-tenant
error isolation — a tenant-less global reap over a multitenant ledger is
impossible by construction (each tenant's call carries its own
pre-flight). Sequential by design: the shared repo pool is the one
cross-tenant starvation surface, so per-tenant sweeps do not contend.
{:ok, %{results: %{"org_a" => {:ok, 3}, "org_b" => {:ok, 0}}, total: 3}} =
AshHooks.reap_all(Ledger, tenants: ["org_a", "org_b"])
@spec reconcile_pending(module(), atom(), keyword()) :: {:ok, [AshHooks.Dispatcher.dispatch_result()]} | {:error, term()}
Recovers eligible outbound rows for a declaration and tenant, including
interrupted enqueue, due retries, and expired send leases. Schedule this
periodically for each outbound declaration and tenant; it preserves retry
timing and attempt counts. See AshHooks.Dispatcher.reconcile_pending/3.
Schema validator for the secret option — accepts only secret SOURCES,
rejecting a literal binary at DSL parse time (ADR-0005).