ADR-0002. Activity notifications are Kafka-fed, persisted in core, then pushed
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-05-20 |
| Deciders | signal-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
ActivityNotificationtable 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:
- Consume
TActivityNotificationMessage; reject unknowneventType. - Resolve recipients server-side —
org/merchantviaPolicyDefinitionRepository,usersvia explicitrecipientIds(fallback[actorId]). - Build
content+htmlfrom a markdown decorator, persist oneActivityNotificationrow per recipient viacreateAll. - Push
observation/signal/notification/createdto eachsignal/notification/{recipientId}room.
The consumer runs with autocommit: false, committing per-message after the handler returns, and fallbackMode: latest.
Consequences
| Pros | Cons |
|---|---|
| Notifications are durable (DB) and live (WS) | Re-delivery creates duplicate rows — no dedup today |
| Producers stay scope-only; Signal owns recipient resolution | Recipient resolution is a synchronous DB hit per message |
Reuses centralized @nx/core schema/repo | Handler errors are logged but not retried/DLQ'd |
| Manual commit gives at-least-once safety | Only PAYMENT_SUCCESS content is implemented so far |
Alternatives Considered
| Option | Pros | Cons | Why rejected |
|---|---|---|---|
| WS-only push (no persistence) | Simplest | Lost if client offline; no history | Bell UI needs durable history |
| Producer resolves recipients + writes rows | Signal stays dumb | Every producer duplicates resolution + schema knowledge | Centralizing in Signal is DRY-er |
| Autocommit + idempotency key | Less manual code | Needs a dedup store; not yet built | Deferred; 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