This document defines the wire protocol used by FediChess clients. Any client (web, mobile, CLI, or other platform) that implements this protocol can discover peers, send and receive challenges, and play games interoperably.
The same action names and JSON payloads apply to all transports; only the wire and discovery differ.
FediChess supports two transports. Clients may use one or both; peers discovered over a given transport are used for that connection (e.g. BLE peer → game over BLE, WebRTC peer → game over WebRTC).
- Discovery and signaling: WebTorrent-compatible trackers (WSS). Clients join a "room" (see below) via Trystero (or a compatible WebRTC matchmaking layer) using the torrent strategy with the same
appIdandrelayUrls. - Data: JSON payloads sent over Trystero actions (peer-to-peer after WebRTC connection). Action type strings must be ≤12 bytes (Trystero constraint).
- Scope: Internet; many peers per room; lobby and game rooms as in the Rooms table below.
- Transport: Bluetooth Low Energy GATT. One FediChess GATT service; one primary characteristic for bidirectional messages (read + notify + write).
- Service UUID:
f47b5e2d-4a9e-4c5a-9b3f-8e1d2c3a4b5c(FediChess BLE service). - Characteristic UUID:
a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d(FediChess messages). - Message format: Each message is UTF-8:
actionName\nfollowed by the JSON payload. Action names are the same as in the Action types table (≤12 bytes). Example:heartbeat\n{"id":"...","elo":1200,"name":"Player","ready":true,"timestamp":123}. If a message exceeds the MTU (e.g. 512 bytes), it may be sent in chunks; the receiver reassembles by buffering until a completeactionName\n{...}is available. - Scope: 1:1 connection per "room". Lobby over BLE = one connected device (one peer); game over BLE = the same connection. No multi-hop mesh in the browser (Web Bluetooth allows 1–2 concurrent GATT connections).
- Peer ID: The remote peer is identified by a stable id (e.g. from an initial handshake, or device id, or a hash). Same payload shapes as WebRTC;
peerIdin payloads refers to this id.
| Room type | Room ID format | Purpose |
|---|---|---|
| Lobby | p2p-chess-global (default) or custom |
Discovery: peers broadcast heartbeats; challenges are sent/received here. |
| Game | p2p-chess-{gameId} |
One game per UUID gameId. Players and spectators join; only the two designated players may send moves or game events. |
- App ID:
p2p-chess-v1(shared by all compatible clients). - Lobby directory (optional): A list of public lobbies (room IDs + optional tracker URLs) can be hosted as static JSON or an API. Clients may fetch this to offer "Global lobby" vs "Community X" and join the same logical pool. Format:
{ "lobbies": [ { "id": "p2p-chess-global", "label": "Global", "trackers": [] } ] }— emptytrackersmeans use client default.
All payloads are JSON. Optional fields may be omitted.
| Action (≤12 bytes) | Direction | Payload shape | Description |
|---|---|---|---|
heartbeat |
Broadcast | HeartbeatPayload |
Periodic presence: ELO, username, ready. |
challenge |
To peer | ChallengePayload |
Request to start a game (challenger sends to one peer). |
challResp |
To peer | ChallengeResponsePayload |
Accept or decline a challenge. |
HeartbeatPayload
{
"id": "<uuid>",
"elo": 1200,
"name": "HKTITAN",
"ready": true,
"timestamp": 1234567890123
}name: Display username (visible to others in the lobby).timestamp: Unix ms; used for ordering/freshness.
ChallengePayload
{
"type": "challenge",
"gameId": "<uuid>",
"challengerId": "self",
"challengerName": "HKTITAN",
"challengerElo": 1200,
"color": "w",
"timestamp": 1234567890123
}gameId: Unique ID for the game; game room will bep2p-chess-{gameId}.color:"w"(challenger plays white) or"b"(challenger plays black).
ChallengeResponsePayload
{ "type": "accept", "gameId": "<uuid>", "timestamp": 1234567890123 }or
{ "type": "decline", "gameId": "<uuid>", "timestamp": 1234567890123 }| Action (≤12 bytes) | Direction | Payload shape | Description |
|---|---|---|---|
role |
To peer(s) | RolePayload |
Announce player (white/black) or spectator; used to establish who may send moves. |
move |
To peer(s) | MovePayload |
New FEN and optional SAN after a move. Only from designated white or black player. |
chat |
To peer(s) | ChatPayload |
Chat message. |
gameEvent |
To peer(s) | GameEventPayload |
Resign, draw offer/accept/decline. Only from designated white or black player. |
sync |
To peer | SyncPayload |
Full FEN sync (e.g. when peer joins; sender is typically white). |
history |
To peer(s) | GameLogEvent |
One append-only game event (move, resign, drawOffer, drawAccept, drawDecline) with seq. |
histSync |
To peer | HistorySyncPayload |
Full game event log for late joiners (players or spectators). |
RolePayload
{ "role": "player", "color": "w", "peerId": "<peer-id>" }or
{ "role": "spectator", "peerId": "<peer-id>" }- Sent on join and when a new peer joins. Establishes
whitePeerIdandblackPeerId; only those two peers may sendmoveandgameEvent. Spectators are read-only.
GameLogEvent (single event in the shared log)
- Move:
{ "seq": 1, "kind": "move", "fen": "<fen>", "san": "e4", "timestamp": 1234567890123 } - Resign:
{ "seq": 2, "kind": "resign", "timestamp": 1234567890123 } - Draw:
{ "seq": 2, "kind": "drawOffer" | "drawAccept" | "drawDecline", "timestamp": 1234567890123 }
HistorySyncPayload
{ "events": [ <GameLogEvent>, ... ] }- Sent to new joiners (including spectators) so they receive the full event log and can derive board state. Receivers apply events in
seqorder; duplicate or out-of-order events are ignored.
MovePayload
{
"type": "move",
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
"san": "e4",
"timestamp": 1234567890123
}ChatPayload
{
"type": "chat",
"text": "Hello",
"peerId": "<sender-peer-id>",
"timestamp": 1234567890123
}GameEventPayload
One of:
{ "type": "resign", "timestamp": 1234567890123 }{ "type": "drawOffer", "timestamp": 1234567890123 }{ "type": "drawAccept", "timestamp": 1234567890123 }{ "type": "drawDecline", "timestamp": 1234567890123 }
SyncPayload
{
"type": "sync",
"fen": "<standard FEN>",
"timestamp": 1234567890123
}- Used to bring a newly joined peer (e.g. black) up to date; typically sent by white on
onPeerJoin.
- All clients in a lobby join the same room ID (e.g.
p2p-chess-global) with the same app ID and trackers. - Each client sends periodic
heartbeatwith its ELO, username, and timestamp; and subscribes toheartbeatto build a peer list. - Client A picks Client B from the list and sends
challenge(withgameId, color, etc.) to B. - B sends
challRespwithtype: "accept"ortype: "decline"to A. - On accept: both navigate (or programmatically join) the game room
p2p-chess-{gameId}. One plays white, one black (agreed via challenge: challenger is white by default in this implementation).
- Clients join the game room as player (with
color=worcolor=bfrom the challenge) or spectator (e.g.spectate=1or no color). Each joiner sends arolemessage (player + color, or spectator) so all peers knowwhitePeerIdandblackPeerId. - Only the two designated players may send
moveandgameEvent; spectators are read-only. Receivers ignoremove/gameEventfrom any peer that is not white or black. - Shared event log: Each move or game event is broadcast as a
historypayload (with monotonicseq). All participants (players and spectators) receive the same log. New joiners receive the full log viahistSyncso board state is consistent. FEN and result are derived by replaying the log in order. - White sends
sync(initial FEN) and, when the log is non-empty,histSyncto new joiners. - Chat: any participant may send
chat.
- Action type strings: Must be ≤12 bytes (UTF-8). Use e.g.
challRespinstead ofchallengeResponse,histSyncfor history sync. - FEN: Must be valid standard FEN; receivers should validate before applying.
- Timestamps: Unix milliseconds; used for ordering and optional conflict resolution.
- Security: Only accept
moveandgameEventfrom peers that have announced themselves as the white or black player viarole. Spectators must not send move or gameEvent.
- App ID:
p2p-chess-v1 - Default lobby room:
p2p-chess-global - Game room:
p2p-chess-{gameId}wheregameIdis a UUID from the challenge. - BLE service UUID:
f47b5e2d-4a9e-4c5a-9b3f-8e1d2c3a4b5c - BLE characteristic UUID:
a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d