Packages
Event-driven notification system for Ash Framework with multiple transport types
Current section
Files
Jump to
Current section
Files
lib/push_backend.ex
defmodule AshDispatch.PushBackend do
@moduledoc """
Behaviour that a Web Push backend module implements. Configure a
backend in your app config:
config :ash_dispatch, :push_backend, MyApp.Push
The transport (`AshDispatch.Transports.Push`) will call your backend's
`deliver/4` whenever an event channel with `transport: :push` fires.
## Why the encryption lives in your app, not here
Web Push (RFC 8291/8292) needs a VAPID keypair, an ECDH/HKDF
encryption step and a per-endpoint HTTP POST to whatever push service
the browser chose (FCM, Mozilla autopush, WNS…). Those are deployment
concerns — key material, egress rules, retry budget — so ash_dispatch
owns the routing and the audit trail and leaves the protocol to the
consumer, exactly like `AshDispatch.SMSBackend`.
## Implementation contract
- **One receipt fans out to many devices.** Unlike email or SMS there
is no single address: a user has one subscription per browser and
machine. Resolve them from `receipt.user_id` (or from
`receipt.recipient` if your app stores a device token there) and
send to each. The receipt represents the *event delivery*, not one
device.
- Read the payload from `receipt.content` — typically `:title`,
`:body` and the URL your service worker should open on click.
Keep it small: push services cap the encrypted payload (4 KB is the
safe ceiling).
- Update the receipt via Ash:
- Success (at least one device accepted):
`for_update(:mark_sent, %{provider_id: provider_id})`
- No subscriptions at all: `for_update(:skip, %{error_message:
"no_subscriptions"})` — a user who never granted permission is
not a failure.
- Failure: `for_update(:mark_failed, %{error_message: reason})`
- Return `{:ok, updated_receipt}` or `{:error, reason}`.
## Pruning dead subscriptions
Push endpoints expire silently. A push service answers `404` or `410
Gone` once a subscription is dead, and it will keep answering that
forever. Delete the subscription row on those two statuses — a backend
that does not prune accumulates dead endpoints and spends its retry
budget on browsers that no longer exist.
Treat `429` and `5xx` as retryable instead; they are the push service
throttling you, not the subscription being gone.
"""
@callback deliver(
receipt :: struct(),
context :: AshDispatch.Context.t(),
channel :: AshDispatch.Channel.t(),
event_config :: map()
) :: {:ok, struct()} | {:error, term()}
end