AshDispatch.SMSBackend behaviour (AshDispatch v0.8.2)

View Source

The behaviour an SMS backend implements.

config :ash_dispatch, :sms_backend, MyApp.SMS

AshDispatch.SMSBackend.Elks ships with the library and covers 46elks; this behaviour is for every other provider.

Where it is called from

From AshDispatch.Workers.SendSMS, not from the transport. The transport enqueues a job and marks the receipt :scheduled; the worker marks :sending and calls deliver/4. A backend therefore does not have to worry about holding a database transaction open — but it must not assume it runs synchronously with the action that triggered the event either.

Requires an Oban queue named :sms.

The job carries only the receipt id, so the context the worker passes is reconstructed from the receipt: event_id and audience are real, but data and variables are empty. A backend that needs either should read receipt.content, which was frozen when the receipt was created and therefore survives a retry.

The contract

  • Read receipt.recipient (the phone number) and the message body from receipt.content. Use AshDispatch.ContentMap.get_content/2: the column is JSONB, so a freshly built struct carries atom keys while one read back from Postgres carries string keys. The dispatcher writes :message.
  • Send through the provider's API.
  • Mark the receipt yourself. The backend is what knows which failures are permanent:
    • delivered: ReceiptStatus.mark_sent(receipt, %{"id" => provider_id})
    • worth retrying: ReceiptStatus.mark_failed(receipt, reason)
    • never going to work: ReceiptStatus.mark_failed_permanent(receipt, reason)
  • Return {:ok, updated_receipt} or {:error, reason}.

An {:error, _} makes the worker mark :failed and lets Oban retry. A malformed phone number or bad credentials belong in mark_failed_permanent — five retries will not fix them, and while they sit as :failed they only delay telling the person who can.

The recipient field

Without an :sms entry in recipient_fields, every recipient raises "No identifier field configured for sms transport", and the error does not say where to look:

config :ash_dispatch,
  recipient_fields: [
    sms: [identifier: :phone, name: [:display_name, :name]]
  ]

Summary

Callbacks

deliver(receipt, context, channel, event_config)

@callback deliver(
  receipt :: struct(),
  context :: AshDispatch.Context.t(),
  channel :: AshDispatch.Channel.t(),
  event_config :: map()
) :: {:ok, struct()} | {:error, term()}