Architecture: hbf-notifications
C4 Component Diagram
Key Flows
Notification Creation and Real-Time Push
An internal microservice (e.g., hbf-reports) POSTs to POST /organizations/:orgId/notifications/ with a JWT token. NotificationService.create persists the record to MySQL, then calls SseHandlerService.sendNotification. If the target userId has an active SSE connection, the event is pushed to that user's Subject. If userId is null (org-wide notification), it is broadcast to all connected subjects for that organizationId.
SSE Connection Lifecycle
A console user GETs GET /organizations/:orgId/notifications/events?userId=. The @Sse endpoint calls SseHandlerService.getUserStream, which lazily creates a Subject keyed orgId#userId. The controller maps the subject to an Observable<{ data }> for NestJS SSE streaming. On connection close or error, removeUserStream is called to clean up the in-memory entry.
Paginated Listing with Read Status
GET /organizations/:orgId/notifications/ runs a query joining notification left-joined with notification_read_details filtered by userId. A separate count query provides total unread. Results are transformed by NotificationUtils.constructNotificationResult to attach a per-user isRead flag to each notification before returning.
Mark Read / Mark All Read
PATCH .../read/:id inserts a NotificationReadDetails row for (notificationId, userId) only if none exists (idempotent). PATCH .../readAll bulk-inserts read-detail rows for all currently unread notifications of the user in the organization using a single query builder INSERT.
Data Model
notification: storesuserId(nullable for org-wide),organizationId,message,notificationType, optionalnavLinkandiconUrl. Index on(userId, organizationId).notification_read_details: stores(notificationId, userId)with unique constraint. Cascade-deletes when parent notification is deleted. Index on(notification, userId).