Decisions ADR-0018 2026-08-31

A notifier decides; a channel delivers

One consumer of the event spine decides what to notify and to whom, from a table of rules. One port per channel delivers it, and each adapter — OneSignal, SendGrid, SMTP — resolves its own templates. Use cases publish events and nothing else.

What was on the table

The platform notifies people outside the cockpit: an invite, an account verification, a broken integration, an exceeded budget, a thread waiting for an answer. The event spine already exists; communication is a consumer of it. Two things had to be decided: who decides that a notice goes out, and what shape the delivery takes across channels that differ — e-mail has a subject and HTML, SMS has 160 characters.

The paths we weighed

Use cases calling a mailer directly. Rejected: the decision “what to notify” would be scattered over every use case that sends e-mail, with no name and no place.

One port for every channel. Rejected: it would carry the union of all channels’ fields, or the lowest common denominator.

Rendering templates in the domain. Rejected: the port would carry HTML, and a provider’s own template editor could never be used.

One e-mail per attention item. Rejected: noise.

What we chose, and why

Two components. The Notifier, a consumer in the worker, decides what to notify and to whom from a table of rules — event type, kind, recipient resolution, data, link path — and emits a command. A channel port delivers it: Mailer for e-mail, SMSer for SMS, Pusher when push exists. Use cases publish events only.

Template index, resolution and rendering live in the adapter. The port speaks intent — a kind and its data — and each adapter maps it to a template: SendGrid to a template id, SMTP and OneSignal to templates in the repository. The contract suite makes every adapter resolve every kind the domain emits, so a missing template is a failing test rather than a silent non-delivery. Without a credential configured, the adapter prints instead of sending.

Transactional notices — an invite, a verification — send immediately, one per event. Attention notices — a blocked thread, a PR awaiting review, an exceeded budget — hook onto the attention box and go out as a digest after a delay, fifteen minutes by default, only if the item is still open.

Idempotency is per (event, rule, action), so one event may trigger several actions. Link paths are data: a rule’s link accepts placeholders resolved against the notification’s data; an unresolvable placeholder removes the link. The consumer runs in the core, where the channel credentials are.

What it cost

A template exists per adapter, and the contract suite is what keeps them complete. The fifteen-minute delay is a starting value to be calibrated with telemetry.

Since then

Link paths as data were added on 2026-09-01 for the invite; the OneSignal adapter joined SendGrid and SMTP in September. The second factor uses the channels and deliberately not the notifier: a code is a challenge, not a notification.