Notifications & Tasks
How Chronicler reaches a user reliably - one context that owns delivery, an outbox that survives retries, and the tasks that demand attention.
Reaching a user sounds simple and is not. An email can bounce, a device can be offline, a preference can say "not this kind", and the event that triggered a notification may be long pruned by the time a retry runs. Chronicler puts all of that behind one bounded context, so the rest of the system can raise a notification and forget about the machinery.
Raise a fact, let the router decide
A source context does not choose a channel or write an email. It pushes a notification through one opaque async contract, and the event it raises carries only semantic facts, what happened and to whom. A router then decides, at handle time, which channels apply, which template to use, how long it lives, whether it supersedes an earlier one, and whether to batch it. Keeping that decision in one place means a new source of notifications does not have to learn how email works.
Delivery that survives its own failures
Delivery fans out to per-channel workers over a separate, prunable stream: email through a swappable transport, in-app through a durable inbox with a live SSE feed. The reliability details are where the care shows.
The email channel works out the recipient's address at send time, from the user id, rather than trusting an address copied onto the original event. The delivery record is a self-contained outbox row that carries the notification kind and its render parameters, so a retry can re-render the message even after the source event has been pruned. A failure is sorted into permanent or transient: a permanent one is marked dead and recorded, a transient one backs off and is retried by a chore, and anything carrying a one-time token is never retried, because a stale token is worse than a missing email. Crucially, the channel worker never returns an error for a send failure, so one undeliverable address cannot crash-loop and poison the whole worker.
When an operator has to step in
Operators can see the delivery ledger, scoped like the audit log, and act on a stuck message. Revive returns a dead delivery to the retry queue, so a transient outage no longer strands mail forever. Cancel stops a delivery before it goes. Resend re-issues an already-sent notification as a new one, linked to the original. Reading the rendered content of a message is gated behind its own separate permission, because the convenience of a preview should not quietly hand every operator the contents of users' mail.
Tasks are the other half
A notification is a nudge that happens once. A task is a durable item that sits there until it is dealt with: a self-authored todo, a banner the user must acknowledge, an onboarding step, a schema problem to fix. One task aggregate covers all of them through a category and a typed detail, with a shared open-and-resolve lifecycle. A source context can raise a one-off task, or declare the full set of problems for a target so that fixed ones heal themselves. Who sees a task is worked out late, by matching an audience token against the user's live grants. A task may send a notification, but the sending is the notification context's job.
Where this connects
Notifications lean on the durable read-model state described in Observability and operations, and tasks resolve their audience through Authorization.
Read the record for the detail
This page is the guide. The full records under docs/adrs carry the retry classification, the audience-token model, and the alternatives that were weighed and rejected.