# 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`](../../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 serialization** — `handleWebSocketMessage` chains handlers per `connectionId` so `join_server` cannot run while `identify` is still awaiting the token DB lookup. 3. **Client replay on reconnect** — `SignalingManager.reIdentifyAndRejoin` sends `identify` then re-joins rooms; see [`toju-app/src/app/infrastructure/realtime/README.md`](../../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](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](authentication.md) — session tokens and `identify` trust boundary - [direct-messaging.md](direct-messaging.md) — DM envelope relay types - [voice-webrtc.md](voice-webrtc.md) — RTC relay and `voice_state` - [plugins.md](plugins.md) — `plugin_event` / `plugin_requirements` - [message-integrity.md](message-integrity.md) — signed revisions (P2P; `account_sync` relay) - Product client deep dive: [`toju-app/src/app/infrastructure/realtime/README.md`](../../toju-app/src/app/infrastructure/realtime/README.md) - Server handler: [`server/src/websocket/handler.ts`](../../server/src/websocket/handler.ts) ## Changelog | Date | Change | |------|--------| | 2026-07-05 | Initial canonical envelope catalog; deprecates `shared-kernel/signaling-contracts.ts` as wire source |