Skip to content
Registry StackDocsDevelopment (unreleased)

How Registry Messaging works

For the operator

View as Markdown

An SMS can reach delivered while an email stays submitted; the difference is who reports delivery.

You define a package, and an operator defines a runtime file. The package is reviewed configuration that a deployment serves unchanged:

  • Templates. Each version declares a channel, its locales, and its parts, with a JSON Schema for the data a request must carry and a sample messagingctl check renders.
  • Sender profiles. Each fixes a channel to one provider and one sender identity, and says how a failed send is retried, how long a message may wait, and what happens when a send may have reached the provider.
  • Access profiles. Each names the OAuth clients it admits, the scopes their tokens carry, the sender profiles and templates a sender may use, and its rate.

A provider in the package is only a type, smtp or http. An http provider also carries scripts that turn a rendered message into the gateway’s request and read the gateway’s answers. The endpoint, credentials, and callback secret belong to the runtime file, together with the database, the token issuer, the audit journal, and retention.

messagingctl dev writes this runtime file for local runs and points both providers at local stand-ins.

A message. When a caller submits, Messaging resolves the caller’s access profile, checks the data against the template’s schema, renders every part, and stores the message with its recipient and rendered content before it answers 202. Nothing renders later, so a package change never alters a message already accepted.

A worker then sends it. Each try is an attempt, and the message carries two separate facts:

  • Dispatch is what Messaging did: queued, sending, submitted, failed, unknown, cancelled, or expired. submitted means the provider accepted the message.
  • Report is what the provider said afterwards: none, sent, delivered, undelivered, or unavailable, only moving forward. A provider that sends no receipts, such as an SMTP relay, leaves it unavailable.

status is the dispatch state, except that a submitted message reported delivered reads delivered, and one reported undelivered reads failed. An HTTP gateway that posts signed reports moves the status on; an SMTP relay never does.

A send that definitely failed is retried after a delay that doubles each time, up to five attempts by default, until it succeeds, runs out of attempts, or passes its expiry, one day by default.

Three parties own three different answers:

  • The caller’s source of record owns why and when to send, the recipient, and consent. Messaging accepts what a request carries and keeps no contact directory.
  • Messaging owns rendering, the queue, attempts, receipts, retention, and its audit journal. Its access profile decides whether a caller may send; an authorization the caller holds in another product does not carry over.
  • The provider owns delivery. Messaging repeats what the provider reported and never more, which is why the email stops at submitted.

One state belongs to nobody yet. unknown means a send may have reached the provider and nobody can tell. Under the sender profile’s default onUncertain: hold, the worker sends nothing more until an operator who has asked the provider settles it.

A caller reads its own messages with GET /v1/messages/{id}, which names the channel but never the recipient, and cancels one with POST /v1/messages/{id}/cancel while it is still queued. A caller that lost the answer to a submission sends the same body under the same Idempotency-Key and gets the original answer back, never a second message.

An operator works through messagingctl messages: list and show read any message, retry queues a failed one again, settle resolves an unknown one as sent or not sent, and cancel stops a queued one. Each previews first and writes to the audit journal when applied.

Nothing is kept forever. The rendered content and the recipient go first after a message reaches a final state, then the content-free record. The runtime erases what has expired every hour, and messagingctl retention erase-expired does the same on demand.