WebSocket Events

Complete reference for all Socket.IO events.

10 min read

GroundWave 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.