# Protocol Reference
Signal Fish Server uses a JSON-based WebSocket protocol. All messages are JSON objects with a `type` field and
optional `data` field.
MessagePack encoding is also supported for game data when `enable_message_pack_game_data` is enabled.
## Client Messages
### Authenticate
Authenticate with app credentials (required when auth is enabled). App ID is a public identifier that identifies
the game application.
```json
{
"type": "Authenticate",
"data": {
"app_id": "my-game"
}
}
```
Optional fields:
- `sdk_version` - SDK version for debugging and analytics
- `platform` - Platform information (e.g., "unity", "godot", "unreal")
- `game_data_format` - Preferred game data encoding (defaults to JSON text frames)
### JoinRoom
Join or create a room for a specific game. There is no separate room-creation
message. `JoinRoom` behavior depends on `room_code`:
1. Omit `room_code`: create a new room with a generated room code.
2. Provide `room_code` and room exists for that `game_name`: join that room.
3. Provide `room_code` and no room exists for that `game_name`: create a new
room with that room code.
```json
{
"type": "JoinRoom",
"data": {
"game_name": "my-game",
"player_name": "Player1"
}
}
```
Required fields:
- `game_name` - Name of the game
- `player_name` - Name for the player
Optional fields:
- `room_code` - Code used to join/create a room for this `game_name`
- `max_players` - Maximum players (applied only when a new room is created)
- `supports_authority` - Authority support (applied only when a new room is created)
- `relay_transport` - Preferred relay transport protocol (TCP, UDP, or Auto)
### GameData
Send arbitrary game data to other players in the room.
```json
{
"type": "GameData",
"data": {
"data": {
"action": "move",
"x": 100,
"y": 200
}
}
}
```
The outer `data` is the serde content tag. The inner `data` is the variant
field and can be any JSON-serializable object.
### PlayerReady
Toggle your own ready state in the lobby. This message has no payload.
Behavior:
1. First send in `lobby` state marks the player ready.
2. Sending again in `lobby` state marks the player unready.
3. The server broadcasts `LobbyStateChanged` after each toggle.
4. When all players are ready, the server sends `GameStarting`.
```json
{
"type": "PlayerReady"
}
```
This message has no data payload.
If sent while not in a joinable lobby state, the server returns an `Error`
with `INVALID_ROOM_STATE`.
### AuthorityRequest
Request or release game authority.
```json
{
"type": "AuthorityRequest",
"data": {
"become_authority": true
}
}
```
### LeaveRoom
Leave the current room.
```json
{
"type": "LeaveRoom"
}
```
### Ping
Heartbeat ping. Server responds with `Pong`.
```json
{
"type": "Ping"
}
```
### Reconnect
Reconnect to a room after disconnection using authentication token.
```json
{
"type": "Reconnect",
"data": {
"player_id": "player-id",
"room_id": "room-id",
"auth_token": "token-string"
}
}
```
The `auth_token` is generated by the server when a player disconnects and is
provided to the client for reconnection purposes.
### ProvideConnectionInfo
Provide connection info for P2P establishment.
```json
{
"type": "ProvideConnectionInfo",
"data": {
"connection_info": {
"type": "direct",
"host": "192.168.1.10",
"port": 7777
}
}
}
```
### JoinAsSpectator
Join a room as a spectator (read-only observer).
```json
{
"type": "JoinAsSpectator",
"data": {
"game_name": "my-game",
"room_code": "ABC123",
"spectator_name": "Observer1"
}
}
```
Required fields:
- `game_name` - Name of the game
- `room_code` - Code of the room to spectate
- `spectator_name` - Name for the spectator
### LeaveSpectator
Leave spectator mode.
```json
{
"type": "LeaveSpectator"
}
```
This message has no data payload.
## Server Messages
### Authenticated
Authentication successful. Includes app information and rate limits.
```json
{
"type": "Authenticated",
"data": {
"app_name": "my-game",
"organization": "My Organization",
"rate_limits": {
"per_minute": 60,
"per_hour": 3600,
"per_day": 86400
}
}
}
```
Optional fields:
- `organization` - Organization name (if any)
### ProtocolInfo
SDK/protocol compatibility details advertised after authentication.
```json
{
"type": "ProtocolInfo",
"data": {
"capabilities": ["reconnection", "spectators", "authority"],
"game_data_formats": ["json", "message_pack"]
}
}
```
### AuthenticationError
Authentication failed.
```json
{
"type": "AuthenticationError",
"data": {
"error": "Invalid app_id",
"error_code": "INVALID_APP_ID"
}
}
```
### RoomJoined
Successfully joined or created a room. This message is sent both when creating
a new room and when joining an existing room. There is no separate
room-created response type.
```json
{
"type": "RoomJoined",
"data": {
"room_id": "uuid-string",
"room_code": "ABC123",
"player_id": "your-player-id",
"game_name": "my-game",
"max_players": 8,
"supports_authority": true,
"current_players": [
{
"id": "player-id",
"name": "Player 1",
"is_authority": false,
"is_ready": false,
"connected_at": "2024-01-01T00:00:00Z"
}
],
"is_authority": false,
"lobby_state": "waiting",
"ready_players": [],
"relay_type": "WebRTC",
"current_spectators": []
}
}
```
### PlayerJoined
Another player joined the room.
```json
{
"type": "PlayerJoined",
"data": {
"player": {
"id": "player-id",
"name": "Player 2",
"is_authority": false,
"is_ready": false,
"connected_at": "2024-01-01T00:00:00Z"
}
}
}
```
### PlayerLeft
A player left the room.
```json
{
"type": "PlayerLeft",
"data": {
"player_id": "player-id"
}
}
```
### RoomJoinFailed
Failed to join room.
```json
{
"type": "RoomJoinFailed",
"data": {
"reason": "Room is full",
"error_code": "ROOM_FULL"
}
}
```
Note: The `error_code` field is optional.
### RoomLeft
Successfully left room.
```json
{
"type": "RoomLeft"
}
```
This message has no data payload.
### GameData
Game data relayed from another player.
```json
{
"type": "GameData",
"data": {
"from_player": "player-id",
"data": {
"action": "move",
"x": 100,
"y": 200
}
}
}
```
### GameDataBinary
Binary game data payload from another player. Uses bytes for zero-copy cloning during broadcast.
```json
{
"type": "GameDataBinary",
"data": {
"from_player": "player-id",
"encoding": "message_pack",
"payload": "<base64-encoded-bytes>"
}
}
```
### LobbyStateChanged
Lobby state transitioned.
```json
{
"type": "LobbyStateChanged",
"data": {
"lobby_state": "finalized",
"ready_players": ["player-id-1", "player-id-2"],
"all_ready": true
}
}
```
Possible states:
- `waiting` - Waiting for players to join
- `lobby` - Room is full, players coordinating readiness
- `finalized` - All players ready, game starting
### AuthorityChanged
Authority status changed in the room.
```json
{
"type": "AuthorityChanged",
"data": {
"authority_player": "player-id",
"you_are_authority": false
}
}
```
The `authority_player` field can be `null` if no player currently has authority.
### AuthorityResponse
Authority request response.
```json
{
"type": "AuthorityResponse",
"data": {
"granted": true,
"reason": "Authority granted"
}
}
```
Note: The `reason` and `error_code` fields are optional.
### GameStarting
Game is starting with peer connection information.
```json
{
"type": "GameStarting",
"data": {
"peer_connections": [
{
"player_id": "player-id-1",
"player_name": "Player 1",
"is_authority": false,
"relay_type": "WebRTC"
}
]
}
}
```
### Error
An error occurred.
```json
{
"type": "Error",
"data": {
"message": "Room is full",
"error_code": "ROOM_FULL"
}
}
```
Note: The `error_code` field is optional.
Common error codes:
- `ROOM_FULL` - Room has reached max players
- `ROOM_NOT_FOUND` - Room code does not exist
- `INVALID_GAME_NAME` - Game name validation failed
- `RATE_LIMIT_EXCEEDED` - Too many requests
- `AUTHENTICATION_REQUIRED` - Authentication required
- `INVALID_APP_ID` - Invalid app ID
### Pong
Response to client `Ping`.
```json
{
"type": "Pong"
}
```
### Reconnected
Reconnection successful. Includes current room state and missed events.
```json
{
"type": "Reconnected",
"data": {
"room_id": "uuid-string",
"room_code": "ABC123",
"player_id": "your-player-id",
"game_name": "my-game",
"max_players": 8,
"supports_authority": true,
"current_players": [
{
"id": "player-id",
"name": "Player 1",
"is_authority": false,
"is_ready": false,
"connected_at": "2024-01-01T00:00:00Z"
}
],
"is_authority": false,
"lobby_state": "lobby",
"ready_players": ["player-id-1"],
"relay_type": "WebRTC",
"current_spectators": [],
"missed_events": [
{
"type": "GameData",
"data": {
"from_player": "player-id",
"data": {"action": "move"}
}
}
]
}
}
```
### ReconnectionFailed
Reconnection failed.
```json
{
"type": "ReconnectionFailed",
"data": {
"reason": "The reconnection token is invalid or malformed.",
"error_code": "RECONNECTION_TOKEN_INVALID"
}
}
```
### PlayerReconnected
Another player reconnected to the room.
```json
{
"type": "PlayerReconnected",
"data": {
"player_id": "player-id"
}
}
```
### SpectatorJoined
Successfully joined a room as spectator.
```json
{
"type": "SpectatorJoined",
"data": {
"room_id": "uuid-string",
"room_code": "ABC123",
"spectator_id": "your-spectator-id",
"game_name": "my-game",
"current_players": [
{
"id": "player-id",
"name": "Player 1",
"is_authority": false,
"is_ready": false,
"connected_at": "2024-01-01T00:00:00Z"
}
],
"current_spectators": [
{
"id": "spectator-id",
"name": "Observer1",
"connected_at": "2025-01-15T10:35:00Z"
}
],
"lobby_state": "lobby",
"reason": "joined"
}
}
```
Note: The `reason` field is optional.
### SpectatorJoinFailed
Failed to join as spectator.
```json
{
"type": "SpectatorJoinFailed",
"data": {
"reason": "Room not found",
"error_code": "ROOM_NOT_FOUND"
}
}
```
Note: The `error_code` field is optional.
### SpectatorLeft
Successfully left spectator mode.
```json
{
"type": "SpectatorLeft",
"data": {
"room_id": "uuid-string",
"room_code": "ABC123",
"reason": "voluntary_leave",
"current_spectators": []
}
}
```
Note: All fields are optional.
### NewSpectatorJoined
Another spectator joined the room.
```json
{
"type": "NewSpectatorJoined",
"data": {
"spectator": {
"id": "spectator-id",
"name": "Observer2",
"connected_at": "2025-01-15T10:36:00Z"
},
"current_spectators": [
{
"id": "spectator-id-1",
"name": "Observer1",
"connected_at": "2025-01-15T10:35:00Z"
},
{
"id": "spectator-id-2",
"name": "Observer2",
"connected_at": "2025-01-15T10:36:00Z"
}
],
"reason": "joined"
}
}
```
Note: The `reason` field is optional.
### SpectatorDisconnected
Another spectator left the room.
```json
{
"type": "SpectatorDisconnected",
"data": {
"spectator_id": "spectator-id",
"reason": "disconnected",
"current_spectators": []
}
}
```
Note: The `reason` field is optional.
## Session Flow
```text
Client Server
|<-- Authenticated ------------------|
| |
|--- JoinRoom (no room_code) ------->|
|<-- RoomJoined ---------------------|
| |
| (other client joins) |
|<-- PlayerJoined -------------------|
| |
|--- PlayerReady ------------------->|
|<-- LobbyStateChanged (lobby) ------|
| |
|--- GameData ---------------------->|
|<-- GameData (from other player) ---|
| |
|--- LeaveRoom --------------------->|
|<-- RoomLeft -----------------------|
```
## Reconnection Flow
When a client disconnects, the server generates a reconnection token bound to
the player's ID and room. The client uses this token along with the `player_id`
and `room_id` (from the original `RoomJoined` response) to reconnect:
```json
{
"type": "Reconnect",
"data": {
"player_id": "your-player-id",
"room_id": "your-room-id",
"auth_token": "stored-token"
}
}
```
On successful reconnection, the server sends a `Reconnected` message with the current room state and any
missed events that occurred during the disconnection.
## Next Steps
- [Getting Started](getting-started.md) - Basic usage examples
- [Features](features.md) - Complete feature overview