Skip to content

ADR-0002. Activity notifications are Kafka-fed, persisted in core, then pushed

FieldValue
StatusAccepted
Date2026-05-20
Deciderssignal-team
Supersedes

Context

  • Domain events (e.g. payment success) need to become durable, per-user notifications shown in a "bell" UI and a live WS hint.
  • Producers know what happened and a recipient scope (org / merchant / explicit users) but not the concrete recipient list.
  • Signal already owns the WebSocket edge but has no schema; the ActivityNotification table is centralized in @nx/core.
  • Notifications must survive a missed socket — the client may be offline when the event fires.

Decision

We will feed activity notifications to Signal over Kafka (signal.activity-notification), and have the worker resolve → persist → push:

  1. Consume TActivityNotificationMessage; reject unknown eventType.
  2. Resolve recipients server-side — org/merchant via PolicyDefinitionRepository, users via explicit recipientIds (fallback [actorId]).
  3. Build content + html from a markdown decorator, persist one ActivityNotification row per recipient via createAll.
  4. Push observation/signal/notification/created to each signal/notification/{recipientId} room.

The consumer runs with autocommit: false, committing per-message after the handler returns, and fallbackMode: latest.

Consequences

ProsCons
Notifications are durable (DB) and live (WS)Re-delivery creates duplicate rows — no dedup today
Producers stay scope-only; Signal owns recipient resolutionRecipient resolution is a synchronous DB hit per message
Reuses centralized @nx/core schema/repoHandler errors are logged but not retried/DLQ'd
Manual commit gives at-least-once safetyOnly PAYMENT_SUCCESS content is implemented so far

Alternatives Considered

OptionProsConsWhy rejected
WS-only push (no persistence)SimplestLost if client offline; no historyBell UI needs durable history
Producer resolves recipients + writes rowsSignal stays dumbEvery producer duplicates resolution + schema knowledgeCentralizing in Signal is DRY-er
Autocommit + idempotency keyLess manual codeNeeds a dedup store; not yet builtDeferred; manual commit is the interim safety

References

  • signal/src/components/notification/component.ts (consumer config)
  • signal/src/services/activity-notification-worker.service.ts (resolve/persist/push)
  • core/src/common/kafka/types.ts (TActivityNotificationMessage)
  • API Events — idempotency

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.