Files
Toju/agents-docs/features/signaling.md
T
2026-07-14 00:41:05 +02:00

9.3 KiB

Signaling (WebSocket)

Area: realtime Status: Active Last updated: 2026-07-05

Overview

The signaling server exposes a single WebSocket per origin that carries identity, room membership, presence, WebRTC SDP/ICE relay, selected server-relayed chat/DM/voice fallbacks, plugin events, and multi-device account_sync. The product client implements the consumer in toju-app/src/app/infrastructure/realtime/signaling/.

Canonical contract: this document and server/src/websocket/handler.ts. Do not treat toju-app/src/app/shared-kernel/signaling-contracts.ts as authoritative — it lists legacy types (join, leave, chat, ice-candidate) that do not match the live server.

Responsibilities

  • Authenticate connections via identify (session token).
  • Track per-connection room membership and broadcast room-scoped presence.
  • Relay WebRTC offers/answers/ICE between peers that share server membership (or DM/direct-call rules).
  • Relay narrow server fallbacks when P2P data channels are unavailable (chat, DM, voice presence).
  • Forward account_sync payloads to sibling connections for the same user identity.
  • It does NOT own: P2P data-channel payloads (attachments, message inventory, custom emoji chunks, plugin message bus), local persistence, or REST server-directory APIs.

Key concepts

  • Envelope: JSON object with required type string; additional fields vary by type.
  • oderId: user identity on the wire (legacy spelling, matches server code).
  • clientInstanceId: per-tab/device id stored in sessionStorage; multiple open connections per oderId are allowed.
  • connectionScope: optional string grouping connections (e.g. browser profile).
  • voiceActive: server marks the connection that owns outbound RTC relay for a user; updated from voice_state payloads.

Ordering invariants

  1. identify before anything else — unauthenticated connections receive auth_required for all types except identify and keepalive.
  2. Per-connection serializationhandleWebSocketMessage chains handlers per connectionId so join_server cannot run while identify is still awaiting the token DB lookup.
  3. Client replay on reconnectSignalingManager.reIdentifyAndRejoin sends identify then re-joins rooms; see toju-app/src/app/infrastructure/realtime/README.md.

Connection lifecycle (server → client)

On connect the server assigns a connectionId and may emit:

type When
connected Immediately after WebSocket open (server/src/websocket/index.ts)

On disconnect, if the connection was voice-active, the server broadcasts a cleared voice_state via finalizeVoiceDisconnectForConnection.

Inbound types (client → server)

type Auth Behavior
keepalive Optional Responds keepalive_ack with serverTime
identify N/A (establishes auth) Validates session token; sets oderId, profile fields; evicts stale same (oderId, connectionScope, clientInstanceId) sockets; emits account_sync_peer_online to siblings
join_server Required Access check; adds serverId to connection; sends server_users + plugin_requirements; may broadcast user_joined (identity-aware)
view_server Required Switches viewedServerId; refreshes server_users + plugin_requirements
leave_server Required Removes membership; may broadcast user_left with remaining serverIds
offer, answer, ice_candidate Required Relay to targetUserId when peers share server membership
direct-message, direct-message-status, direct-message-mutation, direct-message-typing, direct-message-sync-request, direct-message-sync, direct-call Required Relay to targetUserId (DM rules — no shared-server requirement)
server_icon_peer_request, server_icon_peer_data Required Relay when both users share serverId membership
chat_message Required Broadcast to server members (excludes sender connection)
voice_state Required Updates voiceActive / snapshot; broadcast to server members
voice_client_takeover Required Notifies sibling connections via notifyOtherConnectionsForOderId
account_sync Required Forwards payload object to other connections for same oderId
typing Required Broadcast user_typing to server members
status_update Required Broadcast status_update (online | away | busy | offline) to joined servers
server_icon_available Required Records local iconUpdatedAt per server on the connection
server_icon_sync_request Required Responds server_icon_sync_peers with peers having newer icons
plugin_event Required Validates against server plugin metadata; broadcast or plugin_error

Unknown inbound types are logged and ignored.

identify request fields

Field Required Notes
token Yes Session token from REST login/register
oderId No If present, must match token user id
displayName No Defaults to existing or "User"
description, profileUpdatedAt, homeSignalServerUrl No Profile card fields
clientInstanceId No Per-tab id for multi-device and voice ownership
connectionScope No Eviction scope for stale sockets

Errors: auth_error with MISSING_TOKEN, INVALID_TOKEN, or USER_ID_MISMATCH.

Server-emitted types (server → client)

type Trigger
keepalive_ack Response to keepalive
auth_required Message before identify
auth_error Failed identify
access_denied join_server rejected (serverId, reason)
server_users After join/view; lists unique users in room
user_joined New identity in server (excludes same identity's connections)
user_left Identity fully left server (serverIds = remaining rooms)
plugin_requirements Plugin install snapshot after join/view
plugin_error Invalid plugin event
server_icon_sync_peers Response to server_icon_sync_request
account_sync_peer_online Sibling connection came online
account_sync Relayed multi-device payload
chat_message Relayed room chat fallback
user_typing Typing indicator
status_update Presence status change
voice_state Voice roster / disconnect cleanup
voice_client_takeover Another tab took voice ownership
plugin_event Broadcast plugin event
Forwarded RTC/DM Copies of client messages with fromUserId set

Relay rules

  • RTC (offer / answer / ice_candidate): forwarded when sender and target share any server membership.
  • Direct signaling types: forwarded to targetUserId without shared-server check.
  • Server icon P2P: both users must be members of message.serverId.
  • Broadcasts (chat_message, voice_state, typing, etc.): exclude sender connection id (not whole identity) so multi-device sessions still receive updates.
  • user_joined / user_left: exclude whole identity so other users do not see duplicate join/leave for multiple tabs.

P2P vs signaling split

Transport Carries
WebRTC data channel Chat events, attachments, custom emoji, message revisions/inventory, profile avatar bytes, voice/screen control, plugin message bus, game activity
WebSocket signaling Identity, membership, presence, RTC SDP/ICE, chat/DM/voice fallbacks, account_sync, plugin events

Server-relayed chat (chat_message) and DM types exist so users see written chat and delivery state while data channels are down. Media, attachments, and inventory sync remain peer-plane responsibilities.

Multi-device (account_sync)

The client wraps relayable local changes in account_sync envelopes. The server forwards the inner payload to other open connections for the same oderId. When a device identifies, siblings receive account_sync_peer_online and push snapshots (saved servers, friends, emoji library, chat history batches, etc.). See authentication.md and domain-specific feature docs.

Client implementation map

Concern Location
One socket per signal URL signaling/signaling.manager.ts
Route picker signaling/signaling-transport-handler.ts
Room affinity signaling/server-signaling-coordinator.ts
Inbound dispatch signaling/signaling-message-handler.ts
Constants (intervals, types) realtime.constants.ts

Changelog

Date Change
2026-07-05 Initial canonical envelope catalog; deprecates shared-kernel/signaling-contracts.ts as wire source