Inkress ADRsarchitecture decisions
Overview / Notifications
ADR 0046

Notifications are medium-tagged records fanned to per-channel adapters via a polling outbox

AcceptedNotifications

Context

The platform sends transactional notifications from many domains (order paid, status changes, KYC, etc.) across several channels — email, SMS, push, and outbound webhooks. It needs a uniform way to emit a notification and deliver it through the right channel and provider, with retries.

Decision

Model a notification as one medium-tagged record fanned out to per-channel provider adapters, delivered by a persisted outbox with polling retry.

  • One record, a channel tag. A notification row carries a medium (1 = email, 2 = SMS, 3 = push, 4 = webhook); the same record shape serves every channel.
  • Per-channel provider adapters. Email is sent through an external gateway (MailTrooper) with server-hosted templates rather than an in-process SES/SMTP mailer; SMS runtime-selects between AWS SNS and Neutrino; push goes via FCM.
  • Outbox + polling delivery. A worker reads unsent records (list_unsent) and retries with an attempts counter and backoff — a persisted outbox, not the Oban job pipeline used for webhook delivery (ADR 0014).

Consequences

  • Positive: one record type plus a channel tag covers all notification kinds; adding a channel is a new adapter; email templating/sending is offloaded to the external gateway.
  • Neutral / to revisit — two delivery mechanisms. This outbox+polling path is separate from the Oban delivery used for merchant webhooks and jobs (ADR 0014), so the platform runs two notification delivery mechanisms with different retry semantics; the notification worker's behaviour is also uneven (it always emits the email channel), and the comms layer looks mid-refactor (context modules alias schema modules whose canonical source is the notice schema). Consolidating onto one mechanism is open.
  • Neutral / to revisit: the FCM server key and the Neutrino key/user-id are hard-coded in source — a secrets-hygiene issue to remediate (operational debt, not part of this decision). (The MailTrooper key, by contrast, is read from an env var.) WhatsApp is inbound-only (a merchant lookup for an external chatbot), not an outbound channel.

Alternatives considered

  • An in-process mailer (Bamboo/Swoosh → SES) for email: not adopted — templates and sending are offloaded to the external MailTrooper gateway instead of owning an in-app mail stack.
  • Deliver notifications on Oban (like webhooks, ADR 0014): not adopted here — notifications use their own persisted outbox + polling; the divergence from the Oban path is the noted consolidation debt.
  • A single SMS provider: rejected — SMS runtime-selects between SNS and Neutrino (deliverability / cost by destination).
← 0045 Delivery is a merchant-authored rate matrix; the checkout shipping total is client-supplied 0047 The merchant activity feed is a read-time merge of source tables, not an event store →