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_syncpayloads 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
typestring; 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 peroderIdare 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_statepayloads.
Ordering invariants
identifybefore anything else — unauthenticated connections receiveauth_requiredfor all types exceptidentifyandkeepalive.- Per-connection serialization —
handleWebSocketMessagechains handlers perconnectionIdsojoin_servercannot run whileidentifyis still awaiting the token DB lookup. - Client replay on reconnect —
SignalingManager.reIdentifyAndRejoinsendsidentifythen re-joins rooms; seetoju-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
targetUserIdwithout 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 |
Related
- authentication.md — session tokens and
identifytrust boundary - direct-messaging.md — DM envelope relay types
- voice-webrtc.md — RTC relay and
voice_state - plugins.md —
plugin_event/plugin_requirements - message-integrity.md — signed revisions (P2P;
account_syncrelay) - Product client deep dive:
toju-app/src/app/infrastructure/realtime/README.md - Server handler:
server/src/websocket/handler.ts
Changelog
| Date | Change |
|---|---|
| 2026-07-05 | Initial canonical envelope catalog; deprecates shared-kernel/signaling-contracts.ts as wire source |