Architecture
1. System Context (C4 L1)
2. Container View (C4 L2)
3. Component View (C4 L3) — Internal Layering
| Layer | Responsibility |
|---|---|
| Routes | HTTP surface in RestPaths (/socket/websocket/clients, /notifications) + WS /stream |
| Controllers | Auth + permission gate + recipient scoping (userId from JWT) |
| Services | WS messaging façade, notification CRUD, Kafka message handling |
| Repositories | @nx/core re-exports; PolicyDefinitionRepository resolves recipients |
| Components | Cross-cutting: WebSocket server, Redis, ECDH, Kafka consumer |
4. State Machines Index
Signal has no long-lived stateful domain entity. The closest are the ephemeral WebSocket connection lifecycle and the (binary) notification read state.
| Entity | States | Diagram |
|---|---|---|
| WebSocket connection | CONNECTING, AUTHENTICATED, ENCRYPTED, CLOSED | → jump |
ActivityNotification.isRead | UNREAD, READ | → jump |
WebSocket Connection
| From | Event | To | Guards |
|---|---|---|---|
CONNECTING | authenticate | AUTHENTICATED | type=Bearer + valid JWT via JWKSVerifierTokenService |
AUTHENTICATED | handshake | ENCRYPTED | clientPublicKey present → derive + store AES key |
ENCRYPTED | disconnect | CLOSED | AES key deleted from _clientAesKeys map |
Notification Read State
| From | Event | To | Guards |
|---|---|---|---|
UNREAD | mark-read | READ | recipientId must match JWT subject; sets isRead=true, readAt=now() |
5. Runtime Scenarios
5.1 Encrypted WebSocket Connect
| Step | Detail |
|---|---|
| 2 | Payload field is clientPublicKey; requireEncryption: true rejects connections without it |
| 5 | Per-client ephemeral key pair → forward secrecy; key lives only in memory |
5.2 Activity-Notification Pipeline
| Step | Detail |
|---|---|
| 3 | org → findUserIdsInOrganizer; merchant → findUserIdsInMerchant; users → explicit recipientIds; fallback to [actorId] |
| 8 | Commit happens per-message after the handler returns; on-message error is logged, not retried |
5.3 Cross-Service Push
| Step | Detail |
|---|---|
| 1 | Producers host no WS server — they only publish; signal is the single edge |
| 3 | sendToClient delivers locally if present, else re-publishes to the owning instance |
6. Crosscutting Concerns
| Concern | How this service handles it |
|---|---|
| AuthN (REST) | JWT + Basic strategies, verified via remote JWKS (VerifierApplication) |
| AuthN (WS) | type=Bearer token verified by JWKSVerifierTokenService in the authenticate handler |
| AuthZ | WebSocketClient.* Casbin permissions on client-management routes; /notifications is recipient-scoped (no explicit permission, scoped by JWT subject) |
| Encryption | ECDH P-256 + HKDF-SHA-256 + AES-256-GCM, per-client key in _clientAesKeys map |
| i18n | Notification content built server-side (currently Vietnamese template for PAYMENT_SUCCESS) |
| Logging | Structured key-value (key: %s); Kafka consumer logs topic/partition/offset |
| Idempotency | None at consumer — at-least-once; re-delivery would create duplicate rows (see API Events) |
| Room ACL | validateRoom is currently a passthrough (accepts all rooms); merchant-scoped ACL is a documented TODO |
| IDs | Snowflake via IdGenerator, worker 9 |