PRD: Activity notifications & realtime push
| Module | Platform (CORE-16) | PRD ID | PRD-ACT-001 |
| Status | Shipped | Owner | Phát Nguyễn |
| Date | 2026-06-15 | Version | v1.0 |
| Capability | Notifications · realtime delivery | URD | ACT · WSS |
TL;DR
A noteworthy domain activity becomes a notification that reaches exactly the right people, instantly: the platform fans one activity out to its audience - a whole organizer, a whole merchant, or an explicit user list - persists one record per recipient, and pushes each over an authenticated, end-to-end-encrypted real-time channel. Users read and clear only their own; a dropped channel never loses a record, and operators get a thin set of controls to inspect and steer live connections.
1. Context & Problem
BANA produces a constant stream of noteworthy moments - a payment clears, an order completes - but each stays buried in the service that produced it. There has been no platform-wide way to turn an activity into a durable, per-person notification and surface it live the instant it happens; staff had to refresh and re-query to learn anything had changed.
What was missing is a backbone that decouples producing an activity from delivering it: an event seam any service can publish to, a worker that fans one activity out to the right recipients in the right tenant scope, a durable record that survives a disconnect, and a live transport that pushes each notification to its owner - and only its owner.
This increment ships that backbone as a first-class platform capability: the activity-notification pipeline (ACT) and the realtime stream (WSS) it rides, plus a self-scoped read API and a stable, encrypted delivery contract.
2. Goals & Non-Goals
Goals
- An event-driven pipeline: consume an activity → resolve recipients → render content → persist one record each → push live.
- Scoped recipient resolution - fan one activity to an organizer, 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), and marks one or all read, for their own notifications only.
- A live, authenticated and end-to-end-encrypted push to each recipient's private channel, on a fixed channel/topic contract.
- Resilience - persistence always succeeds; a missing or failing channel never fails the pipeline or blocks other recipients.
- Thin administrative connection controls - status, client listing, targeted send, broadcast, disconnect - behind permissions.
Non-Goals
- Email / SMS / push-notification channels - this increment is in-app realtime only.
- A notification-preferences or per-user mute/subscription model.
- The producers' own logic - publishing the activity is the producer's job; this capability consumes it.
- Cross-tenant channel authorization hardening on the transport (a known follow-up, see §11).
- 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 connection 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 organizer 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 connection 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 successful-payment activity carrying the actor and the order. The worker resolves the recipients for that scope, renders a human-readable message, and writes one notification each. Recipients holding a live connection see it appear instantly on their private channel; an offline recipient finds it waiting on next load. Anyone marks it read - or clears everything at once - and the read-state holds across 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 channel, 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 organizer, whole merchant, or an explicit user list - falling back to the actor when none resolve | URD-ACT-002 |
| FR-3 | Each notification stores its recipient, type, organizer, rendered plain-text + rich-text content, an optional action link, structured data (incl. the actor), and a read flag with timestamp | URD-ACT-003 |
| FR-4 | Content is rendered from the event type into a human-readable message (one rendered plain-text + rich-text string per notification, the same for every recipient); only recognized event types produce a notification, unknown types are skipped without error. Today the single live type renders a fixed Vietnamese string - per-user-language localization is not yet implemented | 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 the realtime transport | URD-WSS-001 |
| FR-9 | Clients connect over an authenticated channel and complete an encrypted key-exchange handshake; post-handshake payloads are end-to-end encrypted | URD-WSS-002 |
| FR-10 | Delivery uses a fixed channel/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 connection controls - status, list clients, get client, broadcast, send-to-channel, 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 organizer / merchant; a user's read API is confined to their own account - 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 and end-to-end encrypted after an encrypted key-exchange handshake |
| Stable subscription contract | A fixed per-recipient channel and a fixed notification topic, so clients subscribe against a stable contract |
| Cross-instance delivery | A cross-instance fan-out delivers to a recipient wherever their connection is established |
| i18n | The notification record stores one rendered plain-text + rich-text string; the content seam is event-type-driven so a renderer can localize later, but the one live type currently emits a fixed Vietnamese string rather than per-recipient-language content |
8. UX & Flows
The client holds an authenticated, encrypted connection and subscribes to its own private channel; on the notification topic, the activity bell is designed to surface each new notification. An offline recipient finds it waiting via the read API on next load, and marking one - or all - read clears the unread count consistently across sessions.
Client surface status: the backend pipeline, read API, and realtime delivery contract are live, but the in-app notification bell in the client footer is currently disabled - it is not yet surfaced to merchant users. The control exists (a left-footer bell that opens a notification sheet) but ships disabled, so end users do not see in-app notifications today; the merchant-facing surface is a follow-up that consumes this already-shipped backbone.
9. Data & Domain
| Entity | Role |
|---|---|
| Notification record | The persisted, per-recipient record - recipient, type, organizer, content (plain + rich text), action link, 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 users within the activity's organizer or merchant scope; explicit lists and actor-fallback are honored |
| Per-recipient channel + topic | The recipient's private real-time channel 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
- Platform core - owns the notification record definition, the recognized event-type registry, the activity-stream topic and message contract, and recipient resolution from membership data.
- Activity event stream - the ingest seam between activity producers and the notification worker.
- Realtime transport + cross-instance fan-out - the authenticated, encrypted, cross-instance live delivery channel.
- Producing services - publish activity events (e.g. the payment path emits a successful-payment activity).
Assumptions
- Producers publish well-formed activity events onto the agreed stream topic.
- Organizer / merchant membership data is available for recipient resolution.
- Clients maintain a live 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 confined to the authenticated user - a user cannot reach another's notifications |
| Cross-tenant channel subscription on the transport | Known hardening follow-up - channel authorization is permissive today; admin/global channels are not emitted to |
| Channel / topic naming drift between producers and clients | Fixed to a per-recipient channel and a fixed notification topic as a stable contract |
| Only one event type live today (a successful payment) | The pipeline is type-driven and reusable; new event types add a renderer without reworking the backbone |
| In-app client surface not yet live | The footer notification bell ships disabled - the backbone (delivery, persistence, read API) is shipped, but merchant users do not see in-app notifications until the client surface is enabled as a follow-up |
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 notification record store; 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 connection controls gated by permissions |
| Monitoring | Notification volume, delivery latency, live-connection health, recipient-resolution correctness, push-failure rate |
13. FAQ
How is a notification's audience chosen? By the activity's recipient scope - the whole organizer, the whole merchant, or an explicit user list. If none resolves to anyone, the activity's actor receives it.
What 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 on connect and complete an encrypted key exchange; every payload after the handshake is end-to-end encrypted.
Can a user see someone else's notifications? No - every list, count, and mark-read is confined to the authenticated user.
What if the realtime push fails? Delivery degrades to persistence-only - the record is already written, and one recipient's failure never blocks the others.
Only payments today? The pipeline is event-type-driven. A successful payment is the first live type; new 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 - URD
- Developer: @nx/signal · @nx/core