In-app notifications (persisted in the DB), real-time delivery over WebSocket, cross-replica presence tracking in Redis, and SMS delivery through pluggable providers.
| File | Purpose |
|---|---|
notifications.controller.ts |
REST inbox for the current user |
notifications.service.ts |
Create, schedule, read and delete notifications; offline delivery |
notifications.gateway.ts |
Socket.IO gateway on namespace /notifications |
redis-presence.service.ts |
"Is user X connected on any replica?" |
sms.service.ts |
SmsService + Twilio / AWS SNS / Mock providers, opt-out handling |
All require JwtAuthGuard and operate only on the caller's notifications.
| Method | Path | Description |
|---|---|---|
GET |
/notifications |
List |
GET |
/notifications/unread-count |
Unread count |
PATCH |
/notifications/:id/read |
Mark one as read |
PATCH |
/notifications/read-all |
Mark all as read |
DELETE |
/notifications/:id |
Delete |
POST |
/notifications/ws-ticket |
Issue a short-lived WebSocket handshake ticket (#1294) |
-
Namespace:
/notifications. CORS origins come fromCORS_ORIGINS. -
Authenticated handshake (#1294). The client first calls
POST /notifications/ws-ticketwith its bearer token. The server returns a short-lived (60 s) ticket bound to the caller's session and device fingerprint. The socket then connects with the ticket:const { ticket } = await fetch('/notifications/ws-ticket', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, }).then((r) => r.json()); io('/notifications', { auth: { ticket } });
-
On connect the gateway verifies the ticket signature, checks the device fingerprint, confirms the session is still active, and joins both
user:{userId}andsession:{sessionId}rooms. Invalid, expired, or revoked-session tickets are disconnected. -
Revocation. When a session is revoked (
DELETE /sessions/:idorDELETE /sessions),SessionRevocationServicenotifies every replica via Redis pub/sub; the gateway emitssession:revokedand drops all sockets bound to that session. A 10 s heartbeat also re-validates sessions as a safety net. -
Legacy fallback. A
userIdquery parameter is still accepted for backward compatibility, but it is unauthenticated, logs a warning, and should be migrated to ticket auth. -
Server → client events include user-targeted notifications (
sendToUser) and broadcast events such asdocument:expired. -
On connect,
deliverPending(userId)flushes notifications created while the user was offline.
Redis keys (both with 30 s TTL):
presence:user:{userId} HASH socketId → pod id
presence:socket:{socketId} STRING userId (reverse lookup)
- The gateway refreshes TTLs every 10 s, so an entry survives two missed heartbeats and expires after a pod crash.
cleanupStaleEntriesruns@Cron(EVERY_MINUTE)and deletes empty user hashes.- The pod ID comes from
HOSTNAME, thenPOD_NAME, then falls back topid-<pid>.
The provider is selected by SMS_PROVIDER:
| Value | Provider | Required env |
|---|---|---|
mock (default) |
Logs only | none |
twilio |
Twilio REST API | TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER |
aws |
AWS SNS | AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY |
SmsService honours user opt-outs, stored in a JSON file at SMS_OPTOUT_STORAGE (default ./data/sms-optout.json). Provider errors are returned as { success: false, error }, not thrown.
| Variable | Purpose |
|---|---|
CORS_ORIGINS |
Comma-separated allowed origins for the WebSocket |
REDIS_HOST / REDIS_PORT / REDIS_PASSWORD |
Presence store |
SMS_PROVIDER |
mock / twilio / aws |
TWILIO_*, AWS_* |
Provider credentials (see table above) |
SMS_OPTOUT_STORAGE |
Opt-out list file (default ./data/sms-optout.json) |
HOSTNAME / POD_NAME |
Replica identity for presence |
Fraud (security alerts), Backup (failure alerts), Transactions (handleTransactionUpdate) and Documents (expiry broadcasts) all publish through this module.
- WebSocket auth is trust-the-client. Any client can connect with another user's
userIdand receive their real-time notifications. Verify a JWT fromhandshake.auth.tokeninhandleConnectionbefore relying on this channel for sensitive data. - The opt-out list is a local file. On multi-replica or ephemeral-disk deployments, opt-outs are not shared and can be lost on redeploy.
document:expiredis emitted to all connected sockets (server.emit), not to the document owner. Don't put sensitive fields in that payload.- The default
mockSMS provider silently "succeeds". Production must setSMS_PROVIDERexplicitly. - Scheduled notifications (
scheduleNotification) are persisted and can be cancelled withcancelScheduledNotification(id).
notifications.gateway.spec.ts, redis-presence.service.spec.ts. Run with npx jest src/notifications.