Skoolsewa - School Management System Docs
Developer ResourcesRealtime

Realtime Features and Flows

Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for Realtime.

Realtime Features and Flows

1. Documentation Evidence

Source TypeFiles or DocsWhat Was Extracted
Contractapps/api/src/modules/realtime/shared/realtime.contract.tsEvent kinds, sound classes, permission mapping, classifiers.
Publisherapps/api/src/modules/realtime/shared/realtime-publisher.service.tsHow an outbox row becomes an event, persistence, dispatch order.
Streamapps/api/src/modules/realtime/shared/realtime-stream.service.tsFan-out, replay, backpressure, heartbeat, shutdown behavior.
Controllerapps/api/src/modules/realtime/admin/realtime-admin.controller.tsRoutes, auth model, actors.
Sibling docsnotification/feature.mdx, architecture/live-updates-and-push.mdxThe personal notification stream and push, which are a separate mechanism.

2. Feature Summary

FieldValue
ModuleRealtime
SubmoduleN/A
Primary user valueAn operator with the admin panel open sees a toast the moment something they are permitted to read changes, instead of having to poll or refresh.
ActorsAdmin (any authenticated staff session), the outbox dispatcher (system).
Main entry pointsGET /api/admin/realtime/stream, GET /api/admin/realtime/catalogue, the outbox dispatcher's relay call into RealtimePublisherService.publishRelayed.
Main outputsSSE frames on the stream, one row per event in the notification table, a Redis pub/sub message.
Related docsBackend, API, Live updates and push.

3. Actor Matrix

ActorCan DoCannot DoAuth RequirementNotes
Admin (non-superadmin)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.
SuperadminReceive every event on the stream regardless of role-permission rows.N/ASame 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.

4. Capability Matrix

CapabilitySurfaceActorRoute/TriggerState ReadState WrittenLinked API Section
List event kinds and sound classesAdminAdminGET /api/admin/realtime/catalogueREALTIME_EVENT_MAP (in-process constant)NoneAPI §Catalogue
Open the live streamAdminAdminGET /api/admin/realtime/streamRedis history list, role permissionsRedis subscriber registration (in-memory)API §8.2
Publish a relayed outbox eventSystemOutbox dispatcherCall into publishRelayed after commitREALTIME_EVENT_MAPnotification row, Redis history list, Redis pub/sub publishBackend §Services

5. User-Facing Flows

5.1 Opening the stream and receiving a live event

Summary

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.

Preconditions

  • The caller has a valid JWT and an active role (user.activeRole present).
  • The API is not mid-shutdown.

Main Flow

StepActor/SystemActionResultSource
1Admin panel BFFOpens GET /api/admin/realtime/stream with Authorization: Bearer and, on reconnect, Last-Event-ID.Backend resolves the caller's permissions once.realtime-admin.controller.ts
2BackendWrites SSE headers, retry:, and a ready frame.Client knows the connection is live.realtime-stream.service.ts attach()
3BackendRegisters the connection's listener, then reads the Redis history list.Missed/cold-start events queued for replay.replay()
4BackendEmits history as history (no Last-Event-ID) or missed (Last-Event-ID present) frames — never as event.Client marks these read, no toast, no sound.attach()
5BackendOn every new publish, filters by permission and writes an event frame with id:.Client shows a toast per its sound class.deliver() / listener()
6BackendWrites a : heartbeat comment every 20s.Idle proxies do not reap the connection.REALTIME_HEARTBEAT_SECONDS

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
No active roleuser.activeRole missing.Refuse before attaching.403, PERMISSION_USER_CONTEXT_MISSING.
Server drainingshuttingDown is true when the request arrives.End the stream immediately as a 200.EventSource retries after retry: 3000 against the next instance.
Slow consumerresponse.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 connectingAfter 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 connectedA 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 replayreplay() 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 eventNo entry in REALTIME_EVENT_MAP, or classify() returns null.Never reaches a client.No frame, no notification row.

6. Admin Flows

6.1 Reading the event catalogue

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.

7. Lifecycle and State Transitions

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.

9. Data and Side Effects by Flow

FlowDB WritesCache EffectsJobsRealtimeAnalyticsNotifications
Outbox row relayedOne notification row (onConflictDoNothing on outbox_event_id)Redis RPUSH+LTRIM on the history list, INCR on the sequence keyNone — runs inline in the dispatcher's relay stepOne Redis PUBLISH, fanned to every connected admin instanceNoneNone (this module does not send push or email; see the notification module for that)
Stream openNoneRead of the Redis history listNoneRegisters an in-memory listenerNoneNone

10. Error and Recovery Flows

ScenarioTriggerUser/System ExperienceRecoverySource
Missing active roleSession with no active role opens the stream.403 PERMISSION_USER_CONTEXT_MISSING.Re-select an active role, retry.realtime-admin.controller.ts
Publish failurepublishRelayed 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 consumerA 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 / deploybeforeApplicationShutdown 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 removedA 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 closeRedis 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.realtime-control.ts

12.1 Feature Inventory With Minor Behaviors

FeatureMinor BehaviorActorTriggerUser/System ResultBackend Side EffectSource
Sound class overrideclassify() on a definition can downgrade, upgrade, or suppress a sound class based on the payload.SystemAn event with a classify function is relayed.A toast plays a different sound than the definition's default, or does not play at all.None extra — decided before persistence.realtime.contract.ts
Cold-start replayFirst connection (no Last-Event-ID) gets the last 200 events, marked read.AdminOpening the panel for the first time in a session.Panel is not empty on first open; no false unread badge.None.realtime-stream.service.ts
Reconnect replayReconnection (Last-Event-ID present) gets everything after that id, up to 500 buffered, marked unread (missed).AdminA dropped connection, an API restart, a laptop wake.Nothing in that window is silently lost, up to the buffer size.None.realtime-stream.service.ts
Duplicate delivery on redelivery raceThe listener is registered before replay runs, so an event published during the replay round-trip can appear once live and once in replay.AdminA publish lands in the exact window between listener registration and replay resolving.Client sees the same id twice; it must de-duplicate.None — documented as acceptable, since duplicating is recoverable and losing is not.realtime-stream.service.ts
Superadmin bypassSuperadmin sessions skip the permission filter entirely.AdminactiveRole.isSuperadmin true.Receives every event kind regardless of granted permissions.None.realtime-admin.controller.ts, realtime-stream.service.ts

12.3 Business Rules and Policy Traceability

RuleBusiness ReasonActor ImpactEnforced InAPI ImpactBackend ImpactTests
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.canReceiveNo 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.buildEventNew 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).Fewer, more meaningful toasts.realtime.contract.ts, realtime-publisher.service.ts buildEventN/ASuppressed events are never persisted or published.realtime.contract.spec.ts
Permissions are resolved once, at connect time.Re-resolving per event would put a role lookup on the hot fan-out path for every admin on every event.A permission change (grant or revoke) takes effect on the admin's next reconnect, not mid-stream.realtime-admin.controller.ts stream()Documented in the route's own Swagger description.N/AN/A
A stream does not outlive a removed role.A role taken away must stop showing what it could read, not linger until the tab happens to reconnect.A staff member who loses a role has their open streams ended and reconnected; a stream opened just as the role went is ended as soon as it registers.RealtimeStreamService.closeUserStreams via close-user; the session re-read in stream()The stream ends as a normal 200; the reconnect is authorised afresh.close-user is best-effort pub/sub; a lost one is logged.realtime-control.int.spec.ts, realtime-stream.service.spec.ts, staff.service.spec.ts

12.4 Tradeoffs and Product Rationale

Product DecisionUser BenefitEngineering BenefitAlternativeTradeoffRisk
Second Redis subscriber connection for SUBSCRIBE, reads through the shared clientLive events keep working alongside every other Redis-backed featureAvoids putting the shared client into subscriber mode, which would break every cache readUse the shared client for bothWould break cache reads process-wide the moment the stream subscribedHigh — the failure mode looks like "Redis is down"
@Res() instead of Nest's @Sse() decoratorReliable heartbeats and a connection that actually drops slow readersBackpressure is only visible through response.writableLength, which @Sse()'s Observable path hidesUse @Sse()Loses the ability to bound a stalled connection's memoryAn unbounded buffer per stalled tab under @Sse()
Fan-out on read (permission filter per event) rather than per-recipient rowsNo table growth proportional to staff countOne row per event regardless of how many admins can see itInsert one row per eligible recipient at publish timeA snapshot problem: a permission granted later would never surface the event to that adminMedium, accepted deliberately (see notifications.ts schema comment)
Fire-and-forget publish, after the outbox transaction commitsA publish failure never blocks or retries the underlying business actionSimple failure model: log and move onMake publishing part of the outbox transactionA missed toast is possible; the truth is always the underlying screens on refreshLow — every screen is correct without the stream

12.5 Flow Edge-Case Matrix

FlowEdge CaseTriggerExpected BehaviorUser/System FeedbackSource
Stream openNo Last-Event-ID, first ever connectionNew admin sessionCold-start replay of last 200, marked readPanel shows recent activity, no false unreadrealtime-stream.service.ts
Stream openLast-Event-ID older than the whole 500-entry bufferLong disconnectReplay returns whatever remains in the bufferClient cannot be told more was missed; must trust the screensreplay()
PublishRedis publish succeeds but history write already happenedNormal path — history is written before publish, deliberatelyA reconnecting client can see the event in history even if the live publish is missedNo loss; the ordering is chosen so this is the safe directionrealtime-publisher.service.ts dispatch()
PublishThe same outbox row is redelivered by the dispatcherAt-least-once outbox dispatchnotification insert is a no-op (onConflictDoNothing on outbox_event_id); the Redis publish still fires againA second toast/frame can occur even though no second row is writtenrealtime-publisher.service.ts persist()
CatalogueA permission code once mapped to an event is removed from the catalogueA module is deleted (e.g. the September ecommerce deletion)Compile error — PermissionCode typing forces the map entry out at the same timeN/Arealtime.contract.ts

13. Completion Checklist

  • Every feature, minor action, and submodule capability is listed.
  • Every actor has allowed and forbidden behavior.
  • Every major flow includes steps, branches, and a diagram.
  • The only lifecycle (connection state) has a transition table and diagram.
  • Every flow links to the API and backend docs.

See Also