Turns the consumer's resource into the outbound Subscription — which events go to which endpoint, and with which signature envelope.
use Ash.Resource,
data_layer: AshSqlite.DataLayer,
extensions: [AshHooks.Subscription]
subscription do
endpoint_resource(MyApp.WebhookEndpoint)
endThe extension injects: event_types ({:array, :string}, default
["*"]), endpoint_id (uuid — the sole UUID-storage-compatible primary
key of the endpoint_resource, under any attribute name), and signing_mode
(:legacy | :dual | :standard, NULLABLE — the outbound declaration's mode
applies when unset: ADR-0002's per-subscription mode). A subscription's own
primary key follows the same single UUID-storage-compatible contract because
outbound delivery rows store it in subscription_id. UUIDv7 keys satisfy
this contract. Composite and non-UUID keys fail at DSL compilation with the
reference column named in the error.
Typing contract
Events carry canonical STRING types (AshHooks.Event canonicalizes at
construction). event_types rows match by exact string or the bare
wildcard — "*" / :"*" — with entries normalized via to_string/1,
so a consumer-typed register ({:array, :atom}, e.g. a closed
one_of: enum) matches identically to the injected string array: same
wildcard posture, same exact-match semantics, either representation.
A closed-enum register is declared by REDECLARING the attribute — your
own attribute(:event_types, {:array, :atom}, constraints: [...])
replaces the injection entirely (add_new_attribute stands down), so
your enum, your min_length, and your own default apply; the wildcard
default ["*"] is NOT forced on you.
Matching is exact strings plus the bare wildcard entry, evaluated IN MEMORY by the dispatcher after reading through the consumer's primary read action — no array-containment SQL, so the semantics are identical on every data layer.
The package injects NO read action and NO read policies: read surfaces are the consumer's to open through their own domain policies — the dispatcher's internal reads run unauthorized, the inbound reaper's precedent (ADR-0005's consumer-governed posture).
Summary
Functions
Whether a subscription row (event_types) matches an event type (a
canonical string): the bare "*" entry matches everything; any other
entry matches exactly — no glob interpretation. Works on any resource
row carrying the injected event_types attribute.
The subscription-level signing modes: :legacy (legacy envelope only),
:dual (legacy + Standard Webhooks), :standard (SW only).
Functions
Whether a subscription row (event_types) matches an event type (a
canonical string): the bare "*" entry matches everything; any other
entry matches exactly — no glob interpretation. Works on any resource
row carrying the injected event_types attribute.
Entries are normalized before comparing: atoms to strings (a
consumer-typed register — {:array, :atom}, a closed enum — H8), a
"*"/:"*" wildcard in either representation. Without the
normalization an atom-typed register matched NOTHING — zero deliveries,
no error. An entry that is neither binary nor atom simply does not
match (total: one exotic row can never abort the fanout).
@spec signing_modes() :: [atom()]
The subscription-level signing modes: :legacy (legacy envelope only),
:dual (legacy + Standard Webhooks), :standard (SW only).