Skip to content

Events (SSE)

Server-Sent Events stream for real-time updates to session lists and in-call reactions.

Base path: /api/events

Authentication: Required.


How It Works

  1. Backend — Hono streamSSE opens a persistent HTTP connection per client. A dedicated Redis subscriber (via redis.duplicate()) listens on two channels per mentorship: mentorship:{id}:sessions and mentorship:{id}:reactions.
  2. Session mutations — Every POST, PATCH, and DELETE on /api/sessions publishes a session-update event to the sessions channel.
  3. Call reactionsPOST /api/sessions/:id/reactions validates the emoji and publishes a call-reaction event to the reactions channel.
  4. Frontend — The useMentorshipSSE(mentorshipId) hook opens the stream. On session-update it invalidates the queryKeys.mentorships.sessions(mentorshipId) cache; on call-reaction it forwards the payload to call UI callbacks for floating reaction rendering.
  5. Heartbeat — A ping event is emitted every 25 seconds to prevent proxy idle-connection timeouts.

Endpoint

GET /api/events/mentorships/:mentorshipId/events

Opens a persistent SSE stream for a specific mentorship. The connection stays open until the client disconnects.

Access rules:

  • PlatformAdmin can connect to any mentorship's stream.
  • All other users must be the coach or teacher of the mentorship.

Path parameters:

Parameter Type Description
mentorshipId string Mentorship UUID

Response headers:

Content-Type: text/event-stream
Cache-Control: no-cache
X-Accel-Buffering: no

X-Accel-Buffering

The X-Accel-Buffering: no header prevents Nginx from buffering the stream. Required for Dokku / reverse-proxy deployments.


Event Types

session-update

Emitted whenever a session in this mentorship is created, updated, or cancelled.

event: session-update
data: {"action":"updated","mentorshipId":"<uuid>"}
Data field Type Description
action string Always "updated"
mentorshipId string UUID of the affected mentorship

On receipt, the frontend invalidates the session list cache to trigger a background refetch.

call-reaction

Emitted when a participant sends an emoji reaction during a live video call.

event: call-reaction
data: {"emoji":"🎉","userId":"<user-cuid>","sessionId":"<session-uuid>"}
Data field Type Description
emoji string The emoji sent (one of 👍, ❤️, 😂, 🎉, 👏, 🙌)
userId string CUID of the user who sent the reaction
sessionId string UUID of the session where the reaction was sent

ping

Heartbeat event sent every 25 seconds to keep the connection alive.

event: ping
data:

No action is required on receipt.

chat-message

Emitted when a participant sends a new chat message in the mentorship.

event: chat-message
data: {"id":"<uuid>","mentorshipId":"<uuid>","senderId":"<cuid>","body":"Hello!","createdAt":"...","sender":{...}}
Data field Type Description
id string (UUID) Message ID
mentorshipId string (UUID) Mentorship the message belongs to
senderId string CUID of the sender
body string Message text
createdAt string ISO 8601 timestamp
sender object Sender profile (firstName, lastName, email)

On reconnect, clients should call GET /api/mentorships/:id/chat/messages to fetch missed messages.


Example: Connecting from JavaScript

const source = new EventSource(`${API_BASE}/api/events/mentorships/${mentorshipId}/events`, {
  headers: {
    'X-Client-Key': CLIENT_KEY,
    'X-Organization-Id': orgId,
  },
  withCredentials: true,
});

source.addEventListener('session-update', (event) => {
  const payload = JSON.parse(event.data);
  // Invalidate session list cache
  queryClient.invalidateQueries({ queryKey: queryKeys.mentorships.sessions(payload.mentorshipId) });
});

source.addEventListener('call-reaction', (event) => {
  const { emoji, userId, sessionId } = JSON.parse(event.data);
  // Show floating reaction in call UI
  showReaction(emoji, userId);
});

source.addEventListener('ping', () => {
  // No-op — heartbeat only
});

React Native

On native (iOS/Android), use react-native-sse instead of the browser EventSource API. It supports custom headers on all platforms, which the browser EventSource does not.


Error responses — mentorship stream

Code Condition
403 Caller is not a participant of this mentorship (and not a PlatformAdmin)
404 Mentorship not found

Global User Stream

GET /api/events/users/me/events

Opens a persistent SSE stream scoped to the authenticated user. Receives real-time events across all of the user's mentorships — used to update the unread message badge in the navigation tab bar without requiring a specific chat or mentorship screen to be open.

Access rules: Any authenticated user.

Response headers:

Content-Type: text/event-stream
Cache-Control: no-cache
X-Accel-Buffering: no

Global event types

chat-message

Emitted when a new chat message is sent in any mentorship the user belongs to.

event: chat-message
data: {"id":"<uuid>","mentorshipId":"<uuid>","senderId":"<cuid>","body":"Hello!","createdAt":"...","sender":{...}}

Same payload shape as the mentorship-scoped chat-message event. On receipt, the frontend invalidates the chat.conversations cache to refresh unread counts across the app.

ping

Heartbeat event sent every 25 seconds. No action required.

Error responses — global stream

Code Condition
401 Not authenticated