Skip to content

PRD: Activity notifications & websocket push

ModulePlatform (CORE-16)PRD IDPRD-ACT-001
StatusShippedOwnerPlatform / Signal squad
Date2026-06-15Versionv1.0
Packages@nx/signal · @nx/coreURDACT · WSS

TL;DR

Turns a noteworthy domain activity into a notification that reaches exactly the right people, instantly. An activity event (e.g. a successful payment) lands on the platform's activity stream; a worker resolves who should hear about it - everyone in an organization, everyone in a merchant, or an explicit list of users - renders a localized message, persists one notification record per recipient, then pushes each record live to its owner over an authenticated, end-to-end-encrypted WebSocket. Each user reads, counts, and clears only their own notifications through a self-scoped API; if the realtime channel is briefly unavailable the records are still persisted, so nothing is lost. Operators get a thin set of administrative transport controls to inspect and steer live connections.

1. Context & Problem

KICKO produces a constant stream of noteworthy moments - a payment clears, an order completes - but until now those moments stayed buried in the service that produced them. There was no platform-wide way to turn an activity into a durable, per-person notification, decide who should see it, and surface it to a live screen the instant it happened. Staff had to refresh and re-query to find out something had occurred.

What was missing is a single backbone that decouples producing an activity from delivering it: an event seam any service can publish onto, a worker that fans one activity out to the correct recipients within the right tenant scope, a durable record so a notification survives a disconnect, and a live transport that pushes it to its owner - and only its owner - in real time.

This increment delivers that backbone as a first-class platform capability: the activity-notification pipeline (ACT) and the realtime WebSocket stream (WSS) it rides on, with a self-scoped read API and a stable, encrypted delivery contract.

2. Goals & Non-Goals

Goals

  • An event-driven pipeline: consume an activity event → resolve recipients → render content → persist one record per recipient → push live.
  • Scoped recipient resolution - fan a single activity out to an organization, a merchant, or an explicit user list, falling back to the actor when no audience resolves.
  • A durable, self-scoped read API - each user lists, counts (incl. unread), marks-one-read and marks-all-read only their own notifications.
  • A live, authenticated and end-to-end-encrypted WebSocket push to each recipient's private channel, on a fixed room/topic contract.
  • Resilience - persistence always succeeds; a missing or failing realtime channel never fails the pipeline and never blocks other recipients.
  • Thin administrative transport controls - connection status, client listing, targeted send, broadcast, and disconnect, behind permissions.

Non-Goals

  • Email / SMS / push-notification channels - this increment delivers the in-app realtime channel only.
  • A notification-preferences or per-user mute/subscription model.
  • The producing services' own logic - publishing the activity event is the producer's responsibility; this capability consumes it.
  • Cross-tenant room authorization hardening on the transport (a known follow-up, see §11).
  • The consuming bell / activity-feed UI styling - this PRD specifies the API and delivery contract the client renders.

3. Success Metrics

MetricTarget / signal
Delivery latencyActivity event → recipient's live push in near real time (sub-second under normal load)
Recipient accuracyA notification reaches exactly the users in the activity's scope; no cross-tenant leakage
DurabilityEvery recipient has a persisted record even if their socket is down; read-state survives reconnect
Self-scopingA user can only ever read or mutate their own notifications
ResilienceA realtime outage or a single failed push degrades to persistence-only, with zero pipeline failures

4. Personas & Use Cases

PersonaGoal in this feature
Owner / ManagerSee organization or merchant activity surface live, without refreshing
Staff / CashierReceive notifications addressed to them and clear them as read
Connected client (web / POS)Hold a live socket and reflect new notifications + read-state instantly
Platform operatorInspect live connections and steer or cut a session when needed

Core scenario: a payment succeeds and the paying service publishes a PAYMENT_SUCCESS activity carrying the actor and the order subject. The worker resolves the recipients for that activity's scope, renders a human-readable message, and writes one notification per recipient. Each recipient that holds a live socket sees the notification appear instantly on their private channel; a recipient who was offline finds it waiting when they next list their notifications. Any recipient marks it read - or clears everything in one action - and the read-state holds across their sessions.

5. User Stories

  • As an owner, I want activity to appear the instant it happens, so I can watch the business without refreshing.
  • As staff, I want only the notifications addressed to me, so my feed is relevant and never shows another person's activity.
  • As a connected client, I want notifications over a live, encrypted socket, so updates are immediate and private in transit.
  • As a user, I want to count my unread notifications and mark them read - one or all, so my feed reflects what I have seen.
  • As the platform, I want a notification to survive a dropped connection, so an offline user never misses it.
  • As an operator, I want to see and steer live connections, so I can diagnose or cut a session.

6. Functional Requirements

#RequirementURD ref
FR-1An activity event on the platform stream is consumed and turned into one notification per resolved recipientURD-ACT-001
FR-2Recipients are resolved by scope - whole organization, whole merchant, or an explicit user list - falling back to the actor when none resolveURD-ACT-002
FR-3Each notification stores recipient, type, organizer, rendered text + HTML content, optional action URL, structured data (incl. actor), and read flag + timestampURD-ACT-003
FR-4Content is rendered from the event type into a localized human-readable message; only recognized event types produce a notification, unknown types are skipped without errorURD-ACT-004 · URD-ACT-005
FR-5A signed-in user lists their own notifications (paginated / filterable) with total and unread countsURD-ACT-006
FR-6A user can count their notifications (e.g. unread) and mark one read or all read, each carrying a read timestampURD-ACT-007 · URD-ACT-008
FR-7Every read and write is scoped to the authenticated recipient - a user never sees or mutates another's notificationsURD-ACT-009
FR-8On creation, each notification is pushed live to its recipient's private channel over WebSocketURD-WSS-001
FR-9Clients connect over an authenticated socket (JWT bearer) with an ECDH handshake; post-handshake payloads are end-to-end encryptedURD-WSS-002
FR-10Delivery uses a fixed room/topic contract and fans out across server instances; a missing channel or a single failed push degrades to persistence-only without blocking othersURD-WSS-003 · URD-WSS-004 · URD-WSS-005
FR-11Administrative transport controls - status, list clients, get client, broadcast, send-to-room, send-to-client, disconnect - are available behind permissionsURD-WSS-006

Full requirement text and acceptance criteria live in the Platform URD - ACT and WSS. This PRD references them rather than restating them.

7. Non-Functional Requirements

AreaRequirement
Tenancy & isolationRecipient resolution is scoped per organization / merchant; a user's read API is scoped to their own user ID - no cross-tenant or cross-user leakage
DurabilityEvery push is backed by a persisted record; read-state is retrievable after reconnect
Decoupling & scaleThe event seam decouples producers from fan-out; a worker absorbs bursts without blocking producers
ResiliencePersistence is independent of the realtime channel; per-recipient pushes are settled independently so one failure never blocks the rest
ConfidentialityThe live channel is authenticated (JWT) and end-to-end encrypted (ECDH-derived AES) after handshake
Transport stabilityFixed room (per-recipient) and topic (observation.signal.notification.created) naming so clients subscribe against a stable contract
Cross-instance deliveryRedis-backed fan-out delivers to a recipient wherever their socket is connected
i18nUser-facing notification content is rendered in the user's language

8. UX & Flows

The client holds an authenticated, encrypted socket and subscribes to its own per-recipient channel; on observation.signal.notification.created it surfaces the new notification in the activity bell. A recipient who is offline at emit time finds the notification waiting via the read API on next load. Marking one - or all - read clears the unread count consistently across sessions.

9. Data & Domain

EntityRole
ActivityNotificationThe persisted, per-recipient record - recipient, type, organizer, content (text + HTML), action URL, structured data, read flag + timestamp
Activity eventThe inbound message on the activity stream - event type, recipient scope, actor, organizer / merchant, optional explicit recipients, payload
Recipient resolutionLooks up the user IDs within the activity's organization or merchant scope; explicit lists and actor-fallback are honored
Per-recipient channel + topicThe recipient's private WebSocket room and the notification-created topic - the live delivery contract

Conceptual only - full schema and invariants live in the developer domain model.

10. Dependencies & Assumptions

Depends on

  • @nx/core - owns the ActivityNotification schema / model / repository, the recognized event-type registry, the activity-stream topic + message types, and recipient resolution via the policy/membership repository.
  • Activity stream (Kafka) - the ingest seam between activity producers and the notification worker.
  • WebSocket transport + Redis - the authenticated, encrypted, cross-instance live delivery channel.
  • Producing services - publish activity events (e.g. the payment path emits PAYMENT_SUCCESS).

Assumptions

  • Producers publish well-formed activity events onto the agreed stream topic.
  • Organization / merchant membership data is available for recipient resolution.
  • Clients maintain a WebSocket connection and subscribe to their own per-recipient channel.

11. Risks & Open Questions

Risk / questionMitigation / status
A recipient is offline when the activity firesThe record is persisted regardless; the client re-reads it via the self-scoped API on next load
Realtime channel unavailable or a single push failsPipeline degrades to persistence-only; per-recipient pushes are settled independently so one failure never blocks others
Cross-user leakage in the read APIEvery read / write is scoped to the authenticated user ID - a user cannot reach another's notifications
Cross-tenant room subscription on the transportKnown hardening follow-up - room authorization is permissive today; admin/global rooms are not emitted to
Room / topic naming drift between producers and clientsFixed to a per-recipient room and the observation.signal.notification.created topic as a stable contract
Only one event type live today (PAYMENT_SUCCESS)The pipeline is type-driven and reusable; new event types add a renderer without reworking the backbone

12. Release Plan & Launch Criteria

AspectPlan
PhaseP2 - ACT and WSS in the URD feature catalog
RolloutAll merchants; no feature flag
MigrationNew ActivityNotification entity; no data migration
Launch criteriaActivity event → recipient resolution → persisted record → live push verified end to end; self-scoped list / count / mark-read / mark-all-read work; a realtime outage degrades to persistence-only; admin transport controls gated by permissions
MonitoringNotification volume, delivery latency, WebSocket connection health, recipient-resolution correctness, push-failure rate

13. FAQ

How is the audience for a notification chosen? By the activity's recipient scope - the whole organization, the whole merchant, or an explicit list of users. If none of those resolve to anyone, the activity's actor receives it.

What happens if the recipient is offline? The notification is still persisted. The client finds it via the self-scoped read API on next load, and read-state holds across sessions.

Is the live channel secure? Yes - clients authenticate with a JWT bearer over the socket and complete an ECDH key exchange; all payloads after the handshake are end-to-end encrypted.

Can a user see someone else's notifications? No - every list, count, and mark-read is scoped to the authenticated user; the read API never returns another user's records.

What if the realtime push fails? Delivery degrades gracefully to persistence-only - the record is already written, and a failure for one recipient never blocks the others.

Only payments today? The pipeline is event-type-driven. PAYMENT_SUCCESS is the first live type; new activity types plug in a content renderer and reuse the whole backbone.

References

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