PRD: Activity notifications & websocket push
| Module | Platform (CORE-16) | PRD ID | PRD-ACT-001 |
| Status | Shipped | Owner | Platform / Signal squad |
| Date | 2026-06-15 | Version | v1.0 |
| Packages | @nx/signal · @nx/core | URD | ACT · 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
| Metric | Target / signal |
|---|---|
| Delivery latency | Activity event → recipient's live push in near real time (sub-second under normal load) |
| Recipient accuracy | A notification reaches exactly the users in the activity's scope; no cross-tenant leakage |
| Durability | Every recipient has a persisted record even if their socket is down; read-state survives reconnect |
| Self-scoping | A user can only ever read or mutate their own notifications |
| Resilience | A realtime outage or a single failed push degrades to persistence-only, with zero pipeline failures |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner / Manager | See organization or merchant activity surface live, without refreshing |
| Staff / Cashier | Receive 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 operator | Inspect 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
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | An activity event on the platform stream is consumed and turned into one notification per resolved recipient | URD-ACT-001 |
| FR-2 | Recipients are resolved by scope - whole organization, whole merchant, or an explicit user list - falling back to the actor when none resolve | URD-ACT-002 |
| FR-3 | Each notification stores recipient, type, organizer, rendered text + HTML content, optional action URL, structured data (incl. actor), and read flag + timestamp | URD-ACT-003 |
| FR-4 | Content is rendered from the event type into a localized human-readable message; only recognized event types produce a notification, unknown types are skipped without error | URD-ACT-004 · URD-ACT-005 |
| FR-5 | A signed-in user lists their own notifications (paginated / filterable) with total and unread counts | URD-ACT-006 |
| FR-6 | A user can count their notifications (e.g. unread) and mark one read or all read, each carrying a read timestamp | URD-ACT-007 · URD-ACT-008 |
| FR-7 | Every read and write is scoped to the authenticated recipient - a user never sees or mutates another's notifications | URD-ACT-009 |
| FR-8 | On creation, each notification is pushed live to its recipient's private channel over WebSocket | URD-WSS-001 |
| FR-9 | Clients connect over an authenticated socket (JWT bearer) with an ECDH handshake; post-handshake payloads are end-to-end encrypted | URD-WSS-002 |
| FR-10 | Delivery 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 others | URD-WSS-003 · URD-WSS-004 · URD-WSS-005 |
| FR-11 | Administrative transport controls - status, list clients, get client, broadcast, send-to-room, send-to-client, disconnect - are available behind permissions | URD-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
| Area | Requirement |
|---|---|
| Tenancy & isolation | Recipient 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 |
| Durability | Every push is backed by a persisted record; read-state is retrievable after reconnect |
| Decoupling & scale | The event seam decouples producers from fan-out; a worker absorbs bursts without blocking producers |
| Resilience | Persistence is independent of the realtime channel; per-recipient pushes are settled independently so one failure never blocks the rest |
| Confidentiality | The live channel is authenticated (JWT) and end-to-end encrypted (ECDH-derived AES) after handshake |
| Transport stability | Fixed room (per-recipient) and topic (observation.signal.notification.created) naming so clients subscribe against a stable contract |
| Cross-instance delivery | Redis-backed fan-out delivers to a recipient wherever their socket is connected |
| i18n | User-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
| Entity | Role |
|---|---|
ActivityNotification | The persisted, per-recipient record - recipient, type, organizer, content (text + HTML), action URL, structured data, read flag + timestamp |
| Activity event | The inbound message on the activity stream - event type, recipient scope, actor, organizer / merchant, optional explicit recipients, payload |
| Recipient resolution | Looks up the user IDs within the activity's organization or merchant scope; explicit lists and actor-fallback are honored |
| Per-recipient channel + topic | The 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 theActivityNotificationschema / 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 / question | Mitigation / status |
|---|---|
| A recipient is offline when the activity fires | The record is persisted regardless; the client re-reads it via the self-scoped API on next load |
| Realtime channel unavailable or a single push fails | Pipeline degrades to persistence-only; per-recipient pushes are settled independently so one failure never blocks others |
| Cross-user leakage in the read API | Every read / write is scoped to the authenticated user ID - a user cannot reach another's notifications |
| Cross-tenant room subscription on the transport | Known hardening follow-up - room authorization is permissive today; admin/global rooms are not emitted to |
| Room / topic naming drift between producers and clients | Fixed 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
| Aspect | Plan |
|---|---|
| Phase | P2 - ACT and WSS in the URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | New ActivityNotification entity; no data migration |
| Launch criteria | Activity 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 |
| Monitoring | Notification 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
- URD: Platform - ACT · WSS
- Related PRD: Device signal & notifications - the device-monitoring increment that rides this same backbone
- Module: Platform - overview + traceability
- Developer: @nx/signal · @nx/core