Skip to main content

Channel catalog (core.notification_channels, parametric)

email · in_app · webhook · push · sms (adapter F1b behind provider ADR) · whatsapp (F2) · live_activity (F2). The channel row and the NotificationChannelPort exist from day 1 even when the adapter arrives later.

Cascading resolution (levels 0–3 + recipient)

resolveChannels(event, tenant, module, recipient) — PURE function in packages/core, unit-tested: 0. Platform: adapter exists and is operative (Vercel Flags operational flag, mirrored in messaging.platform_channel_status).
  1. Tenant: channel active for this tenant (entitlement + configured credentials/sender domain) — messaging.tenant_channel_settings.
  2. Module: channel enabled for the emitting module in this tenant — messaging.module_channel_settings.
  3. Event type: routing entry (channels, per-channel template, category) — messaging.event_channel_routing. Recipient gate: consent for the channel ∧ not in messaging.suppressions ∧ preference center ∧ (marketing only: quiet hours + frequency caps). Result: one job per resolved channel onto notif.{channel}.{rail} (standards/jobs.md).

Categories & recipient controls

  • Category per event routing: transactional | marketing | product. Transactional is NO-opt-out (only suppression by hard bounce).
  • Member preference center: per channel AND per category. Quiet hours: tenant default, campaign override. Frequency caps: marketing only.

Traceability (end-to-end)

  • Chain: origin event → rule/campaign → notification job → messaging.sendsmessaging.send_status_history (queued→sent→delivered→opened→clicked / bounced / complained / failed) → outgoing webhooks. All linked by correlation_id + origin_event_id.
  • Provider status ingestion via provider webhooks (Resend today); each future channel ships its status adapter.
  • Hard bounces and complaints write to messaging.suppressions automatically. EVERY send checks suppressions — no exceptions.

Rails & fairness

Transactional rail always outranks marketing (jobs.md). Token buckets per provider (global) and per tenant. Blasts: batch endpoints only, throttled; a tenant’s blast MUST NOT degrade another tenant’s OTP latency (this is the Resend shared-limit mitigation, generalized to all channels).