Files
KoalaSync/docs/PROTOCOL.md
T
2026-07-15 06:58:22 +02:00

9.3 KiB

WebSocket Protocol Reference

This document describes the relay behavior implemented by server/index.js and the event names defined in shared/constants.js.

Transport

  • The relay uses Socket.IO v4 events over WebSocket.
  • Long-polling is disabled (transports: ['websocket'], allowUpgrades: false).
  • Messages are Socket.IO event packets whose payload is an event name plus an object payload.
  • The relay caps incoming Socket.IO message size at 4 KB.

Invite fragments

Current invitations use named URL-fragment fields:

#j2:r=<roomId>&p=<password>&k=<base64url-chat-secret>[&u=<relayUrl>]

The fragment is parsed by the website and forwarded to the extension as structured fields. k is client-only and must never appear in a relay payload. Legacy #join:<roomId>:<password>[:1:<relayUrl>] fragments remain valid but provide no chat secret.

Connection Handshake

The Socket.IO handshake must include:

  • token: must match OFFICIAL_SERVER_TOKEN.
  • version: optional app version. If present, it must be a valid semver-like string and not older than MIN_VERSION (default 1.0.0).

If the token is invalid, the relay emits error and disconnects the socket. If version is invalid or too old, the relay emits error and disconnects the socket.

After the socket is connected, join_room must include protocolVersion. It must equal PROTOCOL_VERSION exactly. A mismatch emits error and rejects the join attempt; it does not currently disconnect the socket.

Room Join

join_room (client -> server)

Payload:

{
  "roomId": "string, sanitized to [A-Za-z0-9-], max 64",
  "peerId": "string, max 16",
  "username": "string, max 30",
  "password": "string, max 128, optional",
  "tabTitle": "string, max 100, optional",
  "mediaTitle": "string, max 100, optional",
  "protocolVersion": "string, max 16"
}

Behavior:

  • Creates the room if it does not exist and capacity allows it.
  • The first peer becomes hostPeerId.
  • Rooms may have an optional password hash.
  • Joining with a duplicate peerId disconnects the previous socket for that peer.
  • Joining the same room with the same socket and peer is ignored as a no-op.
  • Switching rooms removes the socket from the old room first.

On success, the joining socket receives room_data. Other room members receive peer_status with status: "joined".

room_data (server -> client)

Payload:

{
  "roomId": "string",
  "peers": ["peer state objects"],
  "activeLobby": "object or null",
  "hostPeerId": "string or null",
  "controlMode": "everyone | host-only",
  "controllers": ["peerId"],
  "capabilities": ["host-control", "co-host"]
}

room_data is sent to the joining socket. It is not the general broadcast used for every later room update.

Room Leave

leave_room (client -> server)

Payload: none.

Behavior:

  • Rate-limited to 10 events per socket per minute.
  • If the socket is mapped to a room, the relay removes it from that room.
  • Remaining room members receive peer_status with status: "left" when the peer is no longer represented by another socket.
  • Empty rooms are deleted.
  • If the host leaves and peers remain, the relay assigns the next peer as host, falls back to controlMode: "everyone", resets controllers to the new host, and broadcasts control_mode.

Exceeding the leave_room limit is logged and the socket is disconnected.

Relayed Room Events

The relay accepts and sanitizes these events, then emits the same event to other peers in the room:

  • play
  • pause
  • seek
  • peer_status
  • force_sync_prepare
  • force_sync_ack
  • force_sync_execute
  • episode_lobby
  • episode_ready
  • episode_lobby_cancel

Relayed payload fields are sanitized and may include:

{
  "senderId": "peerId of sender",
  "seq": "number",
  "currentTime": "number 0..86400 or null",
  "targetTime": "number 0..86400",
  "playbackState": "playing | paused",
  "username": "string, max 30",
  "tabTitle": "string, max 100 or null",
  "mediaTitle": "string, max 100 or null",
  "volume": "number 0..1",
  "muted": "boolean",
  "desynced": "boolean",
  "peerId": "sender peerId",
  "status": "string, max 16",
  "expectedTitle": "string, max 100",
  "title": "string, max 100",
  "actionTimestamp": "number"
}

Undefined fields are removed before relay. Raw client payloads are not forwarded.

Media Control

play, pause, seek

These are room-moving actions. In host-only mode, the relay drops them unless the sender is a controller.

Common payload fields:

  • currentTime for play/pause.
  • targetTime for seek.
  • seq and actionTimestamp when the extension needs stale-command or ACK handling.

The content script applies additional client-side filtering for noisy native player events before it sends these events.

Peer Status

peer_status

Used for heartbeats and peer state updates. The extension sends it every HEARTBEAT_INTERVAL while syncing is active.

Typical fields:

  • peerId
  • username
  • tabTitle
  • mediaTitle
  • playbackState
  • currentTime
  • volume
  • muted
  • desynced
  • status

The relay stores sanitized peer state and relays the sanitized update to other peers.

Force Sync

Force sync coordination is implemented primarily in the extension. The relay sanitizes and relays the events.

force_sync_prepare

Payload includes targetTime. The initiator waits for ACKs or for FORCE_SYNC_TIMEOUT before sending force_sync_execute.

In host-only mode, only controllers may initiate it.

force_sync_ack

The extension sends ACKs with peer identity and sequence data. The relay relays them with the same sanitized relay envelope as other room events, including senderId.

force_sync_execute

Payload includes targetTime. In host-only mode, only controllers may send it. The relay also allows a matching initiator's execute event after that initiator started the prepare step, even if their controller state changed before execute.

Episode Lobby

Episode lobby coordination is implemented primarily in the extension. The relay tracks enough state to include activeLobby in room_data for later joiners.

episode_lobby

Payload uses expectedTitle. The relay creates activeLobby when this field is present and no lobby is already active.

In host-only mode, only controllers may initiate it.

episode_ready

Payload may include title. The relay adds the sender to the active lobby's ready list when a lobby exists.

episode_lobby_cancel

Clears the active lobby and is relayed to peers. In host-only mode, only controllers may initiate it.

Host Control Mode

set_control_mode (client -> server)

Payload:

{
  "controlMode": "everyone | host-only"
}

Only the room host may change the mode. Non-host attempts are ignored and the sender receives the current control_mode snapshot.

Mode changes are debounced per room with CONTROL_MODE_MIN_INTERVAL_MS (500 ms).

set_peer_role (client -> server)

Payload:

{
  "peerId": "string, max 16",
  "controller": "boolean"
}

Only the room host may promote or demote controllers. The host cannot demote themself. Role changes use the same 500 ms per-room debounce as mode changes.

control_mode (server -> client)

Payload:

{
  "controlMode": "everyone | host-only",
  "hostPeerId": "string or null",
  "controllers": ["peerId"]
}

Sent when mode or controller state changes, when host migration changes room authority, and when unauthorized role/mode attempts need to resync the sender.

Room List

get_rooms (client -> server)

Payload: none.

No admin token is required for this Socket.IO event.

Limits:

  • Counts against the per-socket event limit.
  • Also has a 10 second per-socket cooldown.

room_list (server -> client)

Payload:

{
  "rooms": [
    {
      "id": "room id",
      "peerCount": 2,
      "hasPassword": false
    }
  ]
}

Ping, Pong, and ACK

ping

Payload:

{
  "t": 1234567890,
  "target": "peerId, optional"
}

If target is omitted, the relay responds to the sender with pong. If target is another peer in the same room, the relay sends ping to that peer with { "t": ..., "sender": "senderPeerId" }.

pong

Payload:

{
  "t": 1234567890,
  "target": "peerId, optional"
}

If target is a peer in the same room, the relay sends pong to that peer with { "t": ... }.

event_ack

Client payload:

{
  "targetId": "peerId",
  "actionTimestamp": 1234567890
}

If sender and target are still in the same room, the relay emits:

{
  "senderId": "sender peerId",
  "actionTimestamp": 1234567890
}

Rate Limits

  • Connections: 10 per IP per minute; excess connections are disconnected.
  • Relayed/events: 50 per socket per 10 seconds; excess disconnects the socket.
  • get_rooms: 10 second cooldown per socket plus the event limit.
  • leave_room: 10 per socket per minute; excess disconnects the socket.
  • Invalid room passwords: tracked per IP and room. Five recent failures block more password attempts for that room until the failure window ages out.
  • HTTP health and admin-metrics endpoints have their own rate limits outside this Socket.IO protocol.

Capabilities

room_data.capabilities advertises server-backed features:

  • host-control
  • co-host

Clients should treat a missing or unknown capabilities list as unsupported.