WebSocket Events
Complete reference for all Socket.IO events.
10 min readGroundWave uses Socket.IO for all real-time communication. Events follow a domain:action naming convention (e.g. position:update, chat:message). Connect with a JWT token supplied as a query parameter: io({ auth: { token } }). Unauthenticated connections are rejected during the identity handshake.
Connection
These events establish and maintain the authenticated session. The server validates the JWT token, resolves the user record, joins the socket to the appropriate rooms, and then sends the initial roster and application state.
| Event | Direction | Payload |
|---|---|---|
system:identify |
Client → Server |
{ user_id, token } or { token }. Sent by the client immediately after the TCP connection is established. The server uses this to authenticate and register the session.
|
system:identified |
Server → Client |
{ user_id, callsign, role, users }. Sent by the server after successfully validating the identity. users is the current roster array, used to bootstrap the client state without a separate REST call.
|
system:roster |
Server → All | Updated user roster array. Emitted to all connected clients whenever a user connects, disconnects, changes role, or updates their callsign. |
system:error |
Server → Client |
{ event, code, message }. Sent when an event handler fails validation, authorization, or encounters an internal error. The event field echoes the originating event name.
|
Positions
Position events drive blue force tracking. Clients emit their own location at a configurable interval (default 5 seconds) and receive all other users' positions in real time. All coordinates are WGS84 decimal degrees. PostGIS stores positions as GEOGRAPHY(Point, 4326) in (longitude, latitude) order.
| Event | Direction | Payload |
|---|---|---|
position:update |
Client → Server |
{ latitude, longitude, altitude_m?, heading?, speed_mps?, accuracy_m? }. All optional fields are omitted when not available from the device's geolocation API.
|
position:broadcast |
Server → All |
Full position record with user_id, callsign, latitude, longitude, altitude_m, heading, speed_mps, accuracy_m, and recorded_at (ISO timestamp). Persisted to the positions table before broadcast.
|
position:stale |
Server → All |
{ user_id, callsign, last_seen_at }. Emitted when a user's last position fix is older than the staleness threshold (default 30 seconds), signalling the client to dim or mark that user's map marker.
|
Chat
Chat events cover message delivery, channel membership, typing indicators, and channel discovery. Clients must join a channel's Socket.IO room before receiving messages for that channel.
| Event | Direction | Payload |
|---|---|---|
chat:message |
Bidirectional |
Client sends { channel_id, content }. Server persists the message and broadcasts the full record to all channel members: { id, channel_id, content, sender_id, sender_callsign, created_at }.
|
chat:join |
Client → Server |
{ channel_id }. Joins the socket to the specified channel room to begin receiving chat:message and chat:typing events for that channel.
|
chat:leave |
Client → Server |
{ channel_id }. Removes the socket from the channel room. The client stops receiving events for that channel until it rejoins.
|
chat:typing |
Bidirectional |
Client emits { channel_id } while the user is typing. Server broadcasts { channel_id, user_id, callsign } to other channel members. Server-side debounce prevents flooding. The typing indicator auto-clears after 3 seconds of silence.
|
chat:channel_added |
Server → Client | Full channel object. Emitted to a specific user when they are added to a new channel (e.g. when an operator creates a group channel and includes them, or when a DM thread is initiated). |
Markers
Marker events keep the shared map layer synchronized across all clients. All mutation events are broadcast to every connected client regardless of map viewport.
| Event | Direction | Payload |
|---|---|---|
marker:create |
Client → Server |
{ marker_type, geometry: GeoJSON.Geometry, name?, category?, description?, properties? }. The server validates the GeoJSON geometry, persists the marker to PostGIS, then broadcasts marker:created.
|
marker:created |
Server → All |
Full marker record: { id, marker_type, geometry, name, category, description, properties, created_by, created_at }. Emitted after a new marker is successfully persisted.
|
marker:updated |
Server → All |
Full updated marker record. Emitted after a PUT /api/markers/:id request is processed. Clients replace the marker in their local state by matching id.
|
marker:deleted |
Server → All |
{ id, deleted_by }. Emitted after a marker is deleted. Clients remove the matching feature from their local map source.
|
Files
File events notify all clients when the shared file library changes, allowing the file panel to stay current without polling the REST endpoint.
| Event | Direction | Payload |
|---|---|---|
file:created |
Server → All |
Full file metadata record: { id, original_name, mime_type, size_bytes, uploaded_by, uploaded_at }. Emitted after a successful multipart upload.
|
file:deleted |
Server → All |
{ id, deleted_by }. Emitted after a file is deleted. If any active overlays referenced this file, an overlay:removed event is also emitted for each.
|
Overlays
Overlay events synchronize the shared map layer stack across all connected clients in real time. Clients apply or remove MapLibre sources and layers in response to these events.
| Event | Direction | Payload |
|---|---|---|
overlay:created |
Server → All |
Full overlay configuration: { id, file_id, name, overlay_type, style, bbox, corners, created_by, created_at }. Clients add the corresponding MapLibre source and layer.
|
overlay:updated |
Server → All | Updated overlay record with new style properties. Clients apply the updated paint properties (color, opacity, line width) or corner coordinates to the existing MapLibre layer without re-fetching the full data. |
overlay:removed |
Server → All |
{ id }. Emitted after an overlay is disabled. Clients remove the corresponding MapLibre source and layer from the map.
|
Voice
Voice events implement the push-to-talk system. Audio is captured via AudioWorklet as raw 16-bit PCM at 16 kHz in 20 ms frames. The server relays binary chunks to all other participants in the voice channel. A parallel MediaRecorder track preserves WebM/Opus recordings.
| Event | Direction | Payload |
|---|---|---|
voice:join |
Client → Server |
{ channel_id }. Joins the voice channel room and adds the user to the active participants list. Triggers a voice:participants broadcast.
|
voice:leave |
Client → Server |
{ channel_id }. Removes the user from the voice channel room and participants list. Triggers a voice:participants broadcast.
|
voice:ptt-start |
Client → Server |
{ channel_id }. Signals that the user has begun transmitting. Only one transmitter is allowed per channel at a time; subsequent requests receive a voice:error if the channel is already occupied.
|
voice:ptt-stop |
Client → Server |
{ channel_id }. Signals end of transmission. Releases the channel lock and saves the completed WebM/Opus recording to the database.
|
voice:audio-chunk |
Client → Server | Binary PCM frame (20 ms at 16 kHz, 16-bit signed little-endian). Sent continuously while PTT is active. The server relays the chunk to all other room participants via a matching binary Socket.IO event. |
voice:participants |
Server → Room |
Array of current voice participants: [{ user_id, callsign, is_transmitting }]. Broadcast to the channel room whenever the participant list changes.
|
voice:error |
Server → Client |
{ code, message }. Returned when a voice action cannot be completed — e.g. the channel is already occupied, the user lacks permission (observers are listen-only), or the feature toggle is disabled.
|
Federation
Federation events implement the server-to-server data sharing protocol. Peer servers connect using a pre-shared API key and exchange data via the federation:inject event. Echo prevention is enforced at three layers to avoid event loops between federated servers.
| Event | Direction | Payload |
|---|---|---|
federation:connect |
Peer → Server |
Authentication handshake payload: { server_id, api_key, server_name }. The receiving server validates the API key against its configured peer list before accepting the connection.
|
federation:connected |
Server → Peer |
Acceptance response: { server_id, server_name, version }. Sent after a peer's handshake is successfully validated.
|
federation:inject |
Bidirectional |
Data sharing payload: { type, data, origin_server_id }. The type field is one of position, marker, or chat. The origin_server_id is used for echo prevention — servers do not re-broadcast events back to the server that injected them. Injected data is persisted to the local database and re-broadcast to local clients with a server-prefixed callsign.
|
Admin
Admin events are emitted only to sockets that have joined the admin Socket.IO room, which requires the admin role. They power the real-time admin dashboard without polling.
| Event | Direction | Payload |
|---|---|---|
admin:status |
Server → Admin room | Full system telemetry snapshot: uptime, CPU, RAM, disk, Docker container states, connected client count, and version. Emitted on a configurable interval (default 5 seconds) to the admin room. |
admin:log |
Server → Admin room |
A single Pino log entry as it is written: { level, time, msg, ...context }. Streamed in real time from the in-process ring buffer to the admin Logs tab.
|
admin:user_updated |
Server → Admin room |
Updated user record including the new role. Emitted after a role change via PUT /api/users/:id/role. The admin user management table refreshes in place.
|
admin:user_disabled |
Server → Admin room |
{ user_id, callsign }. Emitted after a user is deactivated via DELETE /api/users/:id. Also triggers system:roster to update all clients.
|
admin:user_enabled |
Server → Admin room |
{ user_id, callsign }. Emitted after a previously deactivated user account is re-enabled via the admin panel.
|