Skoolsewa - School Management System Docs
Developer ResourcesRealtime

Realtime API Reference

Complete API contracts for Realtime, including routes, auth, DTOs, responses, errors, examples, and integration notes.

Realtime - API Reference

Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: The admin-facing live event stream and its catalogue. For the personal notification stream and push devices, see /docs/developer/notification/api.

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllerapps/api/src/modules/realtime/admin/realtime-admin.controller.tsRoutes, methods, guards, decorators, status codes.
DTOapps/api/src/modules/realtime/admin/dto/realtime-catalogue-response.dto.tsResponse shape and field descriptions.
Contractapps/api/src/modules/realtime/shared/realtime.contract.tsEvent/permission/sound-class mapping, wire shape of RealtimeEvent.
Stream serviceapps/api/src/modules/realtime/shared/realtime-stream.service.tsFrame types, heartbeat, backpressure, reconnect delay, replay semantics, closing a user's streams, the session re-read.
Control channelapps/api/src/modules/realtime/shared/realtime-control.tsrealtime:control, the close-user message and its publisher.
Publisherapps/api/src/modules/realtime/shared/realtime-publisher.service.tsHistory buffer size, cold-start size, sequence key.
Error registryapps/api/src/common/types/error-codes.tsPERMISSION_USER_CONTEXT_MISSING is the only module-specific code.

2. Module Summary

FieldValue
Module nameRealtime
Module slugrealtime
Primary actorsadmin (any authenticated staff session)
API surfacesadmin
Base route prefix/api/admin/realtime
Auth modelJwtAuthGuard + RoleGuard; the catalogue route additionally requires the System_READ permission; the stream route requires only an authenticated session with an active role — see §5.
PersistencePostgreSQL (notification table), Redis (pub/sub channel, sequence key, history list)
Runtime source of truthThe outbox, relayed into this module after commit. The stream is an enhancement; every admin screen remains correct on a plain refresh with the stream dead.
Sibling docsBackend, Features and flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
kindThe stable, admin-facing name of an event, e.g. feedback.submitted. Deliberately not the outbox's own eventType.realtime.contract.tsCatalogue response, event frames.
soundClassWhich of four tones the admin panel plays: positive, urgent, soft, neutral.realtime.contract.tsCatalogue response, event frames.
permissionThe PermissionCode a subscriber must hold to receive the event — the READ code of the screen the event belongs to.realtime.contract.tsCatalogue response, event frames, canReceive filtering.
Last-Event-IDThe SSE reconnect header. Sent back as the last id: the client saw, to receive only what was missed.realtime-stream.service.tsStream request.
retry:The SSE directive telling EventSource how long to wait before reconnecting on its own. Sent once, at connect, as 3000 ms.realtime-stream.service.ts (REALTIME_RECONNECT_DELAY_MS)Stream response.
Cold start vs reconnectA connection with no Last-Event-ID is a cold start (recent history, marked read, frame type history); one with Last-Event-ID is a reconnect (everything missed, frame type missed).realtime-stream.service.tsStream response.

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
AdminGET/api/admin/realtime/catalogueAdminJwtAuthGuard, RoleGuard, IpThrottlerGuardSystem_READRealtimeAdminController.catalogueEvery event kind the stream can emit, its sound class, and its required permission.
AdminGET/api/admin/realtime/streamAdminJwtAuthGuard, RoleGuardNone declared — allowlisted by design; per-event filtering happens inside the streamRealtimeAdminController.streamLong-lived text/event-stream of admin notifications, filtered per subscriber.

Both routes are declared with no NestJS version segment; the runtime path is /api/admin/realtime/..., matching every other route in this codebase.

5. Auth, Identity, and Permissions

SurfaceGuard/DecoratorIdentity ShapePermissionGuest AllowedNotes
CatalogueJwtAuthGuard, RoleGuard, IpThrottlerGuard (RATE_LIMITS.ADMIN_READ)req.user.activeRoleSystem_READNoAn ordinary permissioned read route.
StreamJwtAuthGuard, RoleGuardreq.user.activeRole (must be present)None declaredNoRoleGuard allows a handler with no @Permissions() only when it is on NO_PERMISSION_ADMIN_HANDLERS, and RealtimeAdminController.stream is on it (role.guard.ts), because it has no permission to declare at the route level — the guard lets it through without reading the active role, the handler itself refuses a request with no active role, and the route is otherwise reachable to any authenticated admin; every individual event is then filtered inside RealtimeStreamService.canReceive against that admin's own resolved permissions. Superadmin bypasses the per-event filter, exactly as RoleGuard bypasses permission checks for superadmin elsewhere.

Why the browser cannot open this route directly. EventSource cannot set an Authorization header. The intended caller is the admin panel's own server (a Next.js route handler acting as a BFF), which holds the session cookie, opens this route server-to-server with an ordinary Authorization: Bearer, and streams the bytes to the browser unmodified. See the client guide for the exact proxy behavior.

Permission resolution timing. The stream route resolves the caller's full permission set once, at connect (RoleService.getPermissionsForRoleId), and holds it for the life of the connection. A permission grant or revocation takes effect on the client's next reconnect, not mid-stream. Two things force that reconnect: removing a role from a staff member ends their open streams through the close-user control message, and a stream whose session's active role changed between authentication and registration is ended as soon as it registers. A change to a role's own permissions on the Roles screen does neither.

6. DTO and Model Reference

6.1 RealtimeCatalogueResponseDto

FieldTypeRequiredDefaultValidationExampleSource
soundClassesRealtimeSoundClass[]YesN/AEnum values only["positive","urgent","soft","neutral"]realtime-catalogue-response.dto.ts
kindsRealtimeEventKindDto[]YesN/AN/ASee belowrealtime-catalogue-response.dto.ts

6.2 RealtimeEventKindDto (nested in kinds[])

FieldTypeRequiredDefaultValidationExampleSource
kindstringYesN/AN/A"order.confirmed" style names in the doc comment; only feedback.submitted, exam.published, exam.result_published, exam.marks_submitted exist todayrealtime-catalogue-response.dto.ts
soundClassRealtimeSoundClassYesN/AEnum"positive"realtime-catalogue-response.dto.ts
permissionstringYesN/AN/A"Feedback_READ"realtime-catalogue-response.dto.ts

6.3 RealtimeEvent (the SSE frame payload, not a DTO class)

FieldTypeRequiredDefaultValidationExampleSource
idnumberYesN/AMonotonic per deployment, from Redis INCR4821realtime.contract.ts
kindstringYesN/AOne of the four currently mapped kinds"exam.result_published"realtime.contract.ts
soundClassRealtimeSoundClassYesN/AEnum"positive"realtime.contract.ts
permissionstring (PermissionCode)YesN/AMust exist in the permission catalogue at compile time"ExamResults_READ"realtime.contract.ts
aggregateIdstringYesN/APublic id of the changed thing"0192f900-..."realtime.contract.ts
occurredAtstring (ISO 8601)YesN/AN/A"2026-09-24T06:10:44.019Z"realtime.contract.ts
summarystringYesN/APre-built server-side; never raw payload"New contact message (support)"realtime-publisher.service.ts buildSummary

8.1 GET /api/admin/realtime/catalogue

Purpose

Every event kind the stream can emit, its sound class, and the permission that gates it — so the admin panel's notification-preferences screen renders from this list instead of a second, hard-coded copy that would drift the moment an event is added or removed.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/realtime/admin/realtime-admin.controller.ts
DTOapps/api/src/modules/realtime/admin/dto/realtime-catalogue-response.dto.ts
Contractapps/api/src/modules/realtime/shared/realtime.contract.ts

Auth and Permissions

JWT, JwtAuthGuard + RoleGuard, permission System_READ. Rate-limited by IpThrottlerGuard at RATE_LIMITS.ADMIN_READ.

Request

Headers only — Authorization. No params, query, or body.

Response

200.

{
  "message": "Realtime catalogue fetched successfully",
  "data": {
    "soundClasses": ["positive", "urgent", "soft", "neutral"],
    "kinds": [
      { "kind": "feedback.submitted", "soundClass": "soft", "permission": "Feedback_READ" },
      { "kind": "exam.published", "soundClass": "neutral", "permission": "Exams_READ" },
      { "kind": "exam.result_published", "soundClass": "positive", "permission": "ExamResults_READ" },
      { "kind": "exam.marks_submitted", "soundClass": "soft", "permission": "ExamMarksVerify_READ" }
    ]
  },
  "errorCode": null
}

Side Effects

None. A pure read of an in-process constant.

Error Cases

HTTP StatusConditionMeaning
401 / 403Missing/invalid auth, or missing System_READGuards
429RATE_LIMITS.ADMIN_READ exceededIP throttled

Edge Cases

  • The list changes only when a code change adds or removes an entry in REALTIME_EVENT_MAP — there is no admin-editable configuration behind it.
  • A permission listed here is guaranteed to exist in the permission catalogue at compile time, because RealtimeEventDefinition.permission is typed as PermissionCode.

Example Requests

curl -s "$API_URL/api/admin/realtime/catalogue" -H "Authorization: Bearer TOKEN"

8.2 GET /api/admin/realtime/stream

Purpose

Long-lived text/event-stream of admin notifications. One connection serves every event kind the caller is permitted to see; the server filters per subscriber rather than exposing a stream per permission.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/realtime/admin/realtime-admin.controller.ts
Stream serviceapps/api/src/modules/realtime/shared/realtime-stream.service.ts
Publisher (history/sequence sizing)apps/api/src/modules/realtime/shared/realtime-publisher.service.ts

Auth and Permissions

JWT, JwtAuthGuard + RoleGuard. No @Permissions() — reachable by any authenticated admin with an active role. Every individual event is then filtered against that admin's resolved permissions (or bypassed entirely for superadmin). Intended to be opened server-to-server by the admin panel's own BFF; see the client guide.

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>.
HeadersNoLast-Event-ID: <number> — send the last frame id seen, on reconnect only.
Params / Query / BodyNo—

Response

200, Content-Type: text/event-stream; charset=utf-8, held open indefinitely. Response headers also include Cache-Control: no-cache, no-transform, Connection: keep-alive, and X-Accel-Buffering: no (defeats nginx's default proxy buffering, which would otherwise hold every event until its buffer fills).

The stream opens with retry: 3000 (how long EventSource waits before its own automatic reconnect), then frames of these types:

Eventid: presentDataWhen
readyNo{ "at": "<ISO timestamp>" }Once, immediately after headers, before replay or any live event.
historyYesA RealtimeEvent (§6.3)Once per event in the cold-start replay (no Last-Event-ID was sent). Up to 200 events, filtered by permission, oldest first. Treat as already read — no toast, no sound.
missedYesA RealtimeEventOnce per event newer than the sent Last-Event-ID, filtered by permission, oldest first, bounded by the 500-event history buffer. Treat as unread — this is exactly what the client did not see while disconnected.
eventYesA RealtimeEventOnce per live publish this subscriber is permitted to see, for as long as the connection is open.

A : comment line (: keep-alive <ISO timestamp>) is written every 20 seconds. It is not a frame — EventSource never surfaces comment lines to application code — and exists only to keep idle-connection-reaping proxies from closing the stream.

Side Effects

None durable from the request itself. Registers an in-memory listener and closer on this API instance for the life of the connection — the closer also indexed by the caller's user id, so a close-user control message can find it — and removes them on disconnect. Right after registering, reads the session's active_role_id once.

Error Cases

HTTP StatusError CodeConditionClient Action
401—Missing or invalid JWTRe-authenticate.
403PERMISSION_USER_CONTEXT_MISSINGAuthenticated but no active roleSelect an active role, retry.

Once the stream is open, no further HTTP error is possible on this connection — a mid-stream problem (slow consumer, shutdown, transport error, a role change) ends the response instead of returning an error status, because the response has already committed to 200.

Edge Cases

  • No cap on concurrent connections per admin. Unlike the personal notification stream (capped at 5 with eviction), this route has no per-user connection limit in the current code.
  • Slow consumer. If more than REALTIME_MAX_BUFFERED_BYTES (1 MiB) is queued in the socket, the connection is closed server-side. The client's EventSource reconnects automatically after retry: 3000 ms; Last-Event-ID on that reconnect fills the gap from the 500-event history buffer.
  • Server shutdown. An in-flight connection is ended immediately as a normal 200 close (never a non-200, which would make EventSource give up permanently) when the process is draining for a deploy. The client reconnects against the replacement instance.
  • Role removed. When a staff role edit removes a role from the caller, every open stream of theirs, on every API instance, is ended through the close-user control message. The client reconnects after retry: 3000 ms with the permissions the session holds now. If the session was acting as the removed role, its active role is gone and the reconnect is refused with 403 PERMISSION_USER_CONTEXT_MISSING until it chooses a role; a client should send the user to the role picker rather than retry. If Redis is unreachable when the role is removed, the close is lost and logged, and the stream keeps its old filter until it next reconnects.
  • Active role changed while connecting. The session's active role is read again after the stream registers. If it is no longer the role whose permissions the stream resolved — removed, or switched in another tab — the stream is ended at once, and the client reconnects under the current role.
  • Duplicate delivery is possible. The listener is registered before the replay read runs, so an event published in that narrow window can arrive once as a live event frame and once in the replay. Clients must de-duplicate on id.
  • History is bounded. A client away longer than 500 events (across the whole deployment, not per-user) receives only what remains in the buffer on reconnect, with no signal that more was missed. Reconcile against the underlying screens, not against the stream, for anything older.
  • Reconnecting with a very old Last-Event-ID (older than everything in the 500-event buffer) is treated the same as no filtering being possible — everything still in the buffer that the permission filter allows is replayed as missed.

Example Requests

curl -N "$API_URL/api/admin/realtime/stream" \
  -H "Authorization: Bearer TOKEN" \
  -H "Accept: text/event-stream"

# Reconnecting after having last seen id 4821:
curl -N "$API_URL/api/admin/realtime/stream" \
  -H "Authorization: Bearer TOKEN" \
  -H "Accept: text/event-stream" \
  -H "Last-Event-ID: 4821"

Example raw frames as written to the wire:

retry: 3000

event: ready
data: {"at":"2026-09-24T06:00:00.000Z"}

event: history
id: 4820
data: {"id":4820,"kind":"feedback.submitted","soundClass":"soft","permission":"Feedback_READ","aggregateId":"0192f900-aaaa-7a10-b3d2-6f1e0c5a7b50","occurredAt":"2026-09-24T05:58:12.000Z","summary":"New contact message (support)"}

: keep-alive 2026-09-24T06:00:20.000Z

event: event
id: 4822
data: {"id":4822,"kind":"exam.result_published","soundClass":"positive","permission":"ExamResults_READ","aggregateId":"0192f900-bbbb-...","occurredAt":"2026-09-24T06:00:25.000Z","summary":"exam.result_published"}

9. Validation and Constraints Summary

ConstraintValueEnforced By
Heartbeat interval20 secondsREALTIME_HEARTBEAT_SECONDS
Slow-consumer buffered-bytes bound1 MiB (1024 * 1024)REALTIME_MAX_BUFFERED_BYTES
Client reconnect delay hint3000 msREALTIME_RECONNECT_DELAY_MS
History buffer size (reconnect replay ceiling)500 eventsREALTIME_HISTORY_LIMIT
Cold-start replay size200 eventsREALTIME_COLD_START_LIMIT
close-user publish timeout2000 ms by default, no offline queueREALTIME_CONTROL_PUBLISH_TIMEOUT_MS (connect timeout: REALTIME_CONTROL_CONNECT_TIMEOUT_MS, also 2000 ms); see RealtimeControlPublisher

10. Integration Checklist

  • The stream is opened server-to-server (BFF pattern), never directly from a browser EventSource, because the browser cannot set Authorization.
  • Last-Event-ID is forwarded on every reconnect the transport layer performs.
  • history and missed frames are rendered as already-read (no toast, no sound); only event frames play a sound, chosen by soundClass.
  • The client de-duplicates on id in case of a redelivery-driven duplicate.
  • A 403 PERMISSION_USER_CONTEXT_MISSING is treated as "select a role," not retried blindly.
  • A stream ended by the server is reconnected, and a reconnect refused with 403 PERMISSION_USER_CONTEXT_MISSING — for example because the session's active role was removed — sends the user to the role picker.
  • The panel remains fully correct if the stream never connects — it is an enhancement layered on data the ordinary read routes already serve.

See Also