Skip to main content

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: stores userId (nullable for org-wide), organizationId, message, notificationType, optional navLink and iconUrl. Index on (userId, organizationId).
  • notification_read_details: stores (notificationId, userId) with unique constraint. Cascade-deletes when parent notification is deleted. Index on (notification, userId).