Open the stream; receive only events whose permission is in their active role's granted set; fetch the catalogue with System_READ.
Receive an event for a permission they do not hold; see another admin's stream.
JwtAuthGuard + RoleGuard, an active role.
Permissions are resolved once, at connect. A permission change takes effect on the next reconnect, not mid-stream — and losing a staff role forces that reconnect at once.
Superadmin
Receive every event on the stream regardless of role-permission rows.
N/A
Same guards; activeRole.isSuperadmin bypasses the per-event filter.
Mirrors RoleGuard's own superadmin bypass so the stream cannot disagree with the endpoints about who sees what.
Outbox dispatcher (system)
Relay a dispatched outbox row into publishRelayed, after its transaction has committed.
Retry a publish failure — it is swallowed and logged.
Internal call, no HTTP surface.
Runs after commit so a publish failure never re-triggers a redelivery of an already-sent job.
An admin panel session opens the SSE stream once, receives a ready frame, then a burst of recent history, then live events for as long as the tab stays open.
EventSource retries after retry: 3000 against the next instance.
Slow consumer
response.writableLength exceeds 1 MiB.
Close that one connection; log a warning.
Client's EventSource reconnects; Last-Event-ID fills the gap.
Active role changed while connecting
After the stream registers, the session's active_role_id is null or no longer the role the stream resolved.
End the stream at once; log at log.
EventSource reconnects under the session's current role, or is refused if it has none.
Role removed while connected
A staff role edit removes one of this admin's roles.
Every open stream of theirs, on every instance, ends through close-user.
EventSource reconnects with the permissions they hold now. If Redis was down when the role was removed, the stream keeps its old filter until it next reconnects.
Redis unreachable during replay
replay() throws.
Log the failure; the live listener is already registered and unaffected.
No frames lost going forward; the caller may miss the replay window only.
Unmapped or suppressed outbox event
No entry in REALTIME_EVENT_MAP, or classify() returns null.
Route: GET /api/admin/realtime/catalogue. Permission: System_READ. Returns every kind/soundClass/permission triple in REALTIME_EVENT_MAP plus the full REALTIME_SOUND_CLASSES list, so the admin panel's notification-preferences screen renders from the backend's own list rather than a second hard-coded copy.
A stream connection has no persisted lifecycle — it is a live, in-memory registration. Its states are: connecting → ready → replaying → live → closed. notification rows persisted from the stream have no lifecycle of their own beyond insert; read state is tracked in notification_read and owned by the notification feed the admin panel's inbox reads (notification-feed module), not by this module.
publishRelayed throws for one row (Redis error, DB error).
That one event is dropped silently for the whole panel; the server log carries the only record.
None automatic — the screens remain correct on refresh; the event is not retried because retrying could duplicate an already-committed side effect.
realtime-publisher.service.ts
Slow consumer
A tab stops reading (suspended laptop, throttled background tab).
That connection is dropped.
EventSource reconnects after retry: 3000; Last-Event-ID fills the gap from history, bounded by 500 events.
realtime-stream.service.ts
API restart / deploy
beforeApplicationShutdown fires with open connections.
Every open stream ends at once, as a 200.
Clients reconnect against the replacement instance; Last-Event-ID fills the gap.
realtime-stream.service.ts
Role removed
A staff role edit removes a role from a connected admin.
Their open streams end; the panel reconnects, and if the session was acting as the removed role the reconnect answers 403 PERMISSION_USER_CONTEXT_MISSING.
Choose a role; the next connect resolves the permissions held now.
realtime-control.ts, realtime-stream.service.ts
Lost stream close
Redis unreachable, or an instance's subscriber reconnecting, when the role is removed.
The stream keeps delivering what the removed role could read; the API logs the lost close at warn.
Resolves on the stream's next reconnect. While Redis is down no events flow at all.
An event reaches a subscriber only if its permission is in that subscriber's granted set.
The stream must never show an admin something they could not already read by opening the screen — it is a security boundary, not a convenience.
An admin without ExamResults_READ never sees an exam.result_published toast.
RealtimeStreamService.canReceive
No per-permission route needed on the stream itself.
Permission set resolved once per connection.
realtime-stream.service.spec.ts
An outbox event reaches the stream only if it has an entry in REALTIME_EVENT_MAP.
The outbox carries the platform's entire internal event traffic; relaying all of it would leak payload contents and turn every new outbox event into an unreviewed notification.
Admins see only deliberately curated events.
RealtimePublisherService.buildEvent
New event kinds require a code change, never a config toggle.
An unmapped event produces no notification row and no publish.
realtime.contract.spec.ts
A classify() function may suppress an event entirely by returning null.
Some outbox events are noise relative to a sibling event that already covers the same fact (a sale moving stock, already covered by an order event).