Socket.io events
The primary channel. Radio state is broadcast here and nowhere else — there is no REST endpoint that returns it.
Connecting
import { io } from 'socket.io-client';
const socket = io('http://localhost:3001', { withCredentials: true });Your client's origin must be listed in CLIENT_ORIGIN on the server, or the connection will not be established.
What arrives immediately
The server sends these on its own, without any request:
sync— the full radio state;radio_hosts_mode— whether live hosts are enabled;usersUpdate— active listeners;radio_hosts_online— only when host mode is enabled.
To obtain an artToken the client must send listener_init — see Build a client in 15 minutes.
Reading the "Sent to" column
| Value | Meaning |
|---|---|
everyone | broadcast to all connections |
single socket | addressed to one client |
room … | only to members of a room, e.g. live hosts |
Your client will not receive room events until it joins the corresponding role — for an ordinary listener that is expected.
Reserved events
Socket.io's own events — connect, disconnect, connect_error and so on — are not listed. They behave as usual and are documented by Socket.io itself.
message and error are filtered out for the same reason: in this codebase they belong to the WebRTC monitoring UDP socket, which also happens to be named socket, not to the Socket.io connection.
How this page is maintained
The event list is generated from the server source, so it is always complete. Descriptions are written by hand in reference/annotations/events.mjs and translated in reference/annotations/en/events.mjs; anything untranslated falls back to Ukrainian rather than going missing.
Describing an event that does not exist in the code makes npm run check fail — that is how renames get caught.
Client → server: 42 events. Server → client: 37 events. Documented in detail: 79 of 79.
Client → server
Events the client sends.
| Event | Description |
|---|---|
admin_active | Upgrades the connection to an admin socket and confirms the session. |
admin_add_song | Adds a track from the library to the queue. |
admin_go_live | Puts an admin on air as a host. |
admin_guest_action | Approves or rejects a guest request. |
admin_insert_song_group | Inserts a predefined group of songs into the queue. |
admin_leave_live | Ends the host's broadcast. |
admin_remove_song | Removes a track from the queue by position. |
admin_skip_song | Skips the current track. |
admin_suggestion_action | Accepts or rejects a listener's song request. |
get_queue | A page of the upcoming queue. |
guest_check_ban | Whether this IP address is banned from the broadcast. |
guest_connect | Connects an approved guest to the broadcast. |
guest_leave_live | The guest leaves the broadcast. |
guest_request | A listener asks to join the broadcast. |
host_audio_chunk | A chunk of microphone audio to mix into the broadcast. |
host_get_background_music_list | A page of available background music. |
host_guest_kick | Removes a guest from the broadcast. |
host_guest_mute | Mutes a guest's microphone. |
host_mic_gain | Changes your microphone level in the mix. |
host_mic_toggle | Turns the on-air microphone on or off. |
host_pause_queue | Pauses the track queue — "just chatting" mode. |
host_resume_queue | Resumes queue playback. |
host_set_background_music | Chooses the track that plays while the queue is paused. |
listener_init | Registers the connection as a listener and asks for an art token. |
moderator_ban_ip | Bans an IP address manually. |
moderator_ban_participant | Bans a broadcast participant by IP. |
moderator_deactivate_guest_code | Deactivates the current access code. |
moderator_generate_guest_code | Creates a special-guest access code. |
moderator_get_banlist | The list of banned IP addresses. |
moderator_get_guest_code | The current special-guest access code. |
moderator_get_live_roster | Who is on air right now. |
moderator_kick | Removes a participant from the broadcast. |
moderator_mute | Mutes any participant of the broadcast. |
moderator_regenerate_guest_code | Replaces the current access code with a new one. |
moderator_unban_ip | Lifts a ban on an IP address. |
monitor_answer | WebRTC answer for the host's personal monitoring channel. |
monitor_ice_candidate | An ICE candidate for the monitoring channel. |
search_queue | Searches the upcoming queue. |
special_guest_connect | Joins the broadcast with a one-off code, skipping the queue. |
stream_get_seek | Asks for the current position of the shared stream (stream mode only). |
stream_ping | Measures round-trip latency to the server. |
suggest_song | Request a song from the library to be played. |
Server → client
Events the client should subscribe to.
| Event | Sent to | Description |
|---|---|---|
admin_authorized | single socket | A helper admin has activated their account with a password of their own. |
admin_confirmed | single socket | Session confirmed; carries the current permissions. |
admin_error | single socket | An admin action failed. |
admin_online | everyone | Whether an admin able to accept requests is online. |
admin_success | single socket | An admin action succeeded. |
audit_new_entry | everyone | A new entry in the audit log. |
background_music_now_playing | room LIVE_HOSTS_ROOM | Which track is currently filling the pause. |
background_music_selection_changed | room LIVE_HOSTS_ROOM | A host picked a different background track. |
background_music_updated | everyone | The background music library has changed. |
donation_result | single socket | Outcome of a donation order: the song was queued or not. |
force_logout | single socket | The session was terminated externally. |
guest_code_updated | single socket | The special-guest access code has changed. |
guest_force_disconnect | single socket | The guest's participation was ended externally. |
guest_pending_status | room LIVE_HOSTS_ROOM | The state of a guest's request while they wait. |
guest_queue_update | single socket | The queue of guest requests. |
guest_request_result | single socket | The decision on a guest's request. |
host_force_disconnect | single socket | The host's broadcast was ended externally. |
host_queue_pause_state | single socket, room LIVE_HOSTS_ROOM | The queue has been paused or resumed. |
jingles_updated | everyone | The set of jingles has changed. |
library_updated | everyone | The library changed: a track was added, edited, moved or deleted. |
listener_uid | single socket | The reply to listener_init: listener identity and art token. |
live_hosts_roster | single socket, room LIVE_HOSTS_ROOM | Who is currently on air: hosts and guests. |
phrases_updated | everyone | The set of phrases has changed. |
privileges_updated | single socket | This admin's permissions have just changed. |
queue_updated | everyone | The queue has changed. |
radio_hosts_mode | single socket | Whether the server was built with live host support. |
radio_hosts_online | everyone, single socket | Whether anyone is on air right now. |
stream_chat_mode_end | everyone | The conversation ended and the queue resumes. |
stream_chat_mode_start | everyone | Hosts paused the queue — conversation has begun. |
stream_jingle_end | everyone | The jingle has finished. |
stream_jingle_start | everyone | A jingle between songs has started. |
stream_track_start | everyone | A new track started in the shared stream. |
suggest_cooldown_update | single socket | Seconds left before another song can be requested. |
suggestion_result | single socket | The admin's decision on your song request. |
suggestions_update | single socket | The current list of listener song requests. |
sync | everyone, single socket | The complete radio state. The core event of the protocol. |
usersUpdate | everyone, single socket | The list of active listeners, for the header roster. |
Details
admin_active
adminUpgrades the connection to an admin socket and confirms the session.
Payload: token?: string — JWT; taken from the adminToken cookie if omitted
The mandatory first step for an admin client: until it happens the socket counts as an ordinary listener and all admin events are ignored silently. The reply is admin_confirmed or admin_error. Privileges are re-read from the database rather than taken from the token, so permission changes apply without logging in again. A previous session of the same admin is disconnected automatically — only one connection per account stays alive.
admin_add_song
adminAdds a track from the library to the queue.
Payload:
{
id: 'day/artist - title.mp3',
title: 'Title', artist: 'Artist',
orderType: 'lastinline' | 'donated',
}Requires the queue_manage privilege. donated places the track ahead of ordinary requests. The result arrives as admin_success or admin_error, not in a callback. A cooldown applies between additions, and the server also refuses if the same track is already adjacent in the queue or the queue no longer fits before the mode switch.
admin_go_live
hostPuts an admin on air as a host.
Payload: no data (the first argument is ignored)
Acknowledgement payload:
{
ok: true,
queuePaused: false,
hosts: [ /* who is on air right now */ ],
pendingGuests: [ /* the queue of guest requests */ ],
backgroundMusicMode: 'random' | 'hostChoice',
selectedBackgroundMusicId: null,
}
{ error: { uk: '…', en: '…' } }Requires the radio_host privilege and RADIO_HOSTS_MODE=true on the server. The number of on-air slots is capped by MAX_LIVE_HOST_SLOTS, and a full room returns an error. Calling it again while already live simply confirms the state. On success the socket joins the hosts room and starts receiving its events.
admin_guest_action
hostApproves or rejects a guest request.
Payload:
{ uid: '…', action: 'accept' | 'reject' }Acknowledgement payload:
{ ok: true } or { error: … }Requires radio_host. If the slot was taken between request and decision, the guest is refused with reason: "room_full".
admin_insert_song_group
adminInserts a predefined group of songs into the queue.
Payload:
{ groupId: string }Requires both queue_manage and settings_groups, exactly like POST /api/admin/song-groups/:groupId/insert. Groups are configured separately through /api/admin/song-groups. Has a longer cooldown than single tracks.
admin_leave_live
hostEnds the host's broadcast.
Payload: no arguments
The slot is freed immediately. Disconnecting has the same effect.
admin_remove_song
adminRemoves a track from the queue by position.
Payload: position: number — offset from the current track, as in search_queue
Requires queue_manage. Has its own cooldown — calling too often returns an error with the number of seconds to wait.
admin_skip_song
adminSkips the current track.
Payload: no arguments
Requires queue_manage. In stream mode the server refuses while a jingle is playing or the queue is paused by a host. Shares a cooldown with adding tracks.
admin_suggestion_action
adminAccepts or rejects a listener's song request.
Payload:
{ uid: '…', action: 'add' | 'reject' }Requires queue_manage. add queues the track as lastinline. The listener receives suggestion_result. If the request already expired, a "not found" error comes back.
get_queue
adminA page of the upcoming queue.
Payload:
{ offset?: number = 0, limit?: number = 10 }Acknowledgement payload:
{
items: [{ id, title, artist, orderType }],
total: 42, // tracks ahead in total
}The reply arrives in the callback. The current track is not included.
guest_check_ban
guestWhether this IP address is banned from the broadcast.
Payload: no data
Acknowledgement payload:
{ banned: boolean }Worth calling before showing the request form, to avoid false hope.
guest_connect
guestConnects an approved guest to the broadcast.
Payload: no data
Acknowledgement payload:
{ ok: true, role: '…', nickname: '…', expiresAt: 1754899200000 }Only succeeds once a host has approved the session and a slot is free. expiresAt is when the session will be cut off; the duration comes from the radio settings. From then on the guest uses the same microphone events as a host.
guest_leave_live
guestThe guest leaves the broadcast.
Payload: no arguments
guest_request
guestA listener asks to join the broadcast.
Payload:
{ nickname: string }Acknowledgement payload:
{ ok: true }
{ error: … , secsLeft?: number }The request joins a queue shown to hosts; the decision arrives as guest_request_result. Admins cannot submit one. A cooldown and an IP ban check apply.
host_audio_chunk
hostA chunk of microphone audio to mix into the broadcast.
Payload: Buffer / ArrayBuffer — raw audio data
Accepted only while the microphone is enabled via host_mic_toggle — otherwise chunks are silently dropped. This is the hottest channel in the protocol: it streams continuously while the host speaks.
host_get_background_music_list
hostA page of available background music.
Payload:
{ offset?: number = 0, limit?: number = 5 }Acknowledgement payload:
{ items: [ … ], total: 12, mode: 'day' }The list depends on the radio's current mode. limit is capped at 50. Non-hosts receive an empty list.
host_guest_kick
hostRemoves a guest from the broadcast.
Payload:
{ targetId: string }Acknowledgement payload:
{ ok: true } or { error: … }host_guest_mute
hostMutes a guest's microphone.
Payload:
{ targetId: string, muted: boolean }Acknowledgement payload:
{ ok: true } or { error: … }host_mic_gain
hostChanges your microphone level in the mix.
Payload:
{ gain: number }host_mic_toggle
hostTurns the on-air microphone on or off.
Payload:
{ on: boolean }While the microphone is off the server discards audio chunks from this socket. Applies to both hosts and guests.
host_pause_queue
hostPauses the track queue — "just chatting" mode.
Payload: no arguments
Available to hosts only, not guests. Background music fills the silence, and in every listener's sync the title becomes "Just chatting".
host_resume_queue
hostResumes queue playback.
Payload: no arguments
host_set_background_music
hostChooses the track that plays while the queue is paused.
Payload:
{ trackId: string | null }Acknowledgement payload:
{ ok: true } or { error: … }null restores random selection. Only meaningful when backgroundMusicMode is hostChoice.
listener_init
listenerRegisters the connection as a listener and asks for an art token.
Payload: no arguments
The server does not hand out the token by itself — until the client sends this event, no listener_uid arrives and art, lyrics and audio stay closed. Admin connections ignore this event.
moderator_ban_ip
adminBans an IP address manually.
Payload:
{ ip: string, nickname?: string }Acknowledgement payload:
{ ok: true, entry: { … } } or { error: … }moderator_ban_participant
adminBans a broadcast participant by IP.
Payload:
{ targetId: string }Acknowledgement payload:
{ ok: true } or { error: … }Works for guests only: hosts cannot be banned. Requires the SQL provider.
moderator_deactivate_guest_code
adminDeactivates the current access code.
Payload: no data
Acknowledgement payload:
{ ok: true }moderator_generate_guest_code
adminCreates a special-guest access code.
Payload:
{ ttlHours?: number }Acknowledgement payload:
{ ok: true, code: "…", expiresAt: 1754899200000 }If an active code already exists an error is returned — deactivate it first, or use moderator_regenerate_guest_code. The code lets someone join the broadcast without queueing, via special_guest_connect.
moderator_get_banlist
adminThe list of banned IP addresses.
Payload:
{ offset?: number = 0, limit?: number = 10 }Acknowledgement payload:
{ list: [ … ], total: 7 }Requires DATA_PROVIDER=sql — otherwise the list is unavailable.
moderator_get_guest_code
adminThe current special-guest access code.
Payload: no data
Acknowledgement payload:
{ code: "…" } — or { code: null } when no code is activemoderator_get_live_roster
adminWho is on air right now.
Payload: no data
Acknowledgement payload:
{ roster: [ … ], hostsOnline: true }Without the privilege an empty roster is returned, not an error.
moderator_kick
adminRemoves a participant from the broadcast.
Payload:
{ targetId: string }Acknowledgement payload:
{ ok: true } or { error: … }The participant receives host_force_disconnect or guest_force_disconnect.
moderator_mute
adminMutes any participant of the broadcast.
Payload:
{ targetId: string, muted: boolean }Acknowledgement payload:
{ ok: true } or { error: … }Unlike host_guest_mute, this also works on hosts.
moderator_regenerate_guest_code
adminReplaces the current access code with a new one.
Payload:
{ ttlHours?: number }Acknowledgement payload:
{ ok: true, code: "…", expiresAt: … }The old code stops working immediately.
moderator_unban_ip
adminLifts a ban on an IP address.
Payload:
{ ip: string }Acknowledgement payload:
{ ok: true } or { error: … }monitor_answer
hostWebRTC answer for the host's personal monitoring channel.
Payload:
{ sdp: … }A private low-latency channel for hearing the broadcast — separate from the shared MP3 stream, which lags. Requires the UDP ports HOST_MONITOR_ICE_PORT_MIN–MAX to be open.
monitor_ice_candidate
hostAn ICE candidate for the monitoring channel.
Payload: a WebRTC candidate object
search_queue
adminSearches the upcoming queue.
Payload:
{ query?: string }Acknowledgement payload:
[{ id, title, artist, orderType, position }]position is the offset from the current track, and it is exactly what admin_remove_song expects. An empty query returns the whole queue.
special_guest_connect
guestJoins the broadcast with a one-off code, skipping the queue.
Payload:
{ code: string, nickname: string }Acknowledgement payload:
{ ok: true, … } or { error: … }The code is issued by a moderator. Attempts are rate-limited, and an expired or deactivated code returns an error.
stream_get_seek
listenerAsks for the current position of the shared stream (stream mode only).
Payload: clientTs: number — the client's local time
Acknowledgement payload:
{ seek: 87.2, duration: 214.5, trackId: 'day/artist - title.mp3' }Only needed to display a timer: in stream mode the audio itself is synchronous by construction. Outside stream mode the server does not reply at all.
stream_ping
listenerMeasures round-trip latency to the server.
Payload: clientTs: number
Acknowledgement payload:
{ serverTs: number, clientTs: number }The server echoes your clientTs unchanged — the round-trip difference gives you a latency estimate for more accurate positioning.
suggest_song
listenerRequest a song from the library to be played.
Payload:
// a song object from GET /api/library
{ id: 'day/artist - title.mp3', title: 'Title', artist: 'Artist' }Acknowledgement payload:
{ ok: true }
{ error: 'cooldown', secsLeft: 240 }
{ error: 'no_admin' } // no online admin with queue rights
{ error: 'no_uid' } // listener_init was never sentUses an acknowledgement callback — the second argument to emit. The error codes here are plain strings, not locale objects, unlike the rest of the API. The cooldown is 5 minutes per uid; a request expires after 5 minutes if no admin responds.
admin_authorized
adminA helper admin has activated their account with a password of their own.
Until then the account has authorized: false and can do nothing.
admin_confirmed
adminSession confirmed; carries the current permissions.
Payload:
{
role: 'super_admin' | 'admin',
privileges: ['queue_manage', '…'],
authorized: true, // false = the helper has not activated themselves yet
}Build your interface from this list rather than from the token contents. authorized: false means a helper admin still has to set their own password.
admin_error
adminAn admin action failed.
Payload: a JSON string containing a locale object
The payload is a string, not an object: JSON.parse it first, then pick a language. The same applies to admin_success.
admin_online
listenerWhether an admin able to accept requests is online.
Payload: boolean
Broadcast to all listeners. false means suggest_song will fail with no_admin, so the request button is worth hiding.
admin_success
adminAn admin action succeeded.
Payload: a JSON string containing a locale object
Often carries a code field for machine handling. See admin_error.
audit_new_entry
adminA new entry in the audit log.
Payload: an entry object — the same shape as in GET /api/admin/audit
Lets you append to the log live instead of re-fetching the whole list. Optional data.details contains before/after changes and context: format and operation types.
background_music_now_playing
hostWhich track is currently filling the pause.
Payload:
{ trackId: '…', filename: '…' } // both null when background music is stoppedbackground_music_selection_changed
hostA host picked a different background track.
Keeps the choice in sync between multiple hosts on air.
background_music_updated
adminThe background music library has changed.
A signal to re-read the list via host_get_background_music_list.
donation_result
listenerOutcome of a donation order: the song was queued or not.
Payload:
{ donationId, accepted: true, tier: 2 | null, song: { title, artist } }
{ donationId, accepted: false, reason: 'payment_failed' | 'no_slot' }Sent only to the socket whose listener_uid matches the donor (both are derived from IP). no_slot means the payment was confirmed but the song could not be queued (e.g. not enough time before a mode switch) — the donation is marked paid_unqueued and stays visible to the admin in the donation history.
force_logout
adminThe session was terminated externally.
Payload:
{ reason: 'admin_deleted' }The account was deleted or revoked. The client should clear its state and return the user to the login screen.
guest_code_updated
adminThe special-guest access code has changed.
A signal for other admins to refresh the code they display, so two moderators do not dictate different codes.
guest_force_disconnect
guestThe guest's participation was ended externally.
Causes: removal, a ban, or the expiresAt time limit. The client must stop the microphone and tear down the broadcast UI.
guest_pending_status
guestThe state of a guest's request while they wait.
guest_queue_update
hostThe queue of guest requests.
Sent only to admins holding the radio_host privilege.
guest_request_result
guestThe decision on a guest's request.
Payload:
{ accepted: true, nickname: '…' }
{ accepted: false, auto: false, reason: 'room_full' }reason: "room_full" means the slot was taken while the request waited. After accepted: true the client must send guest_connect.
host_force_disconnect
hostThe host's broadcast was ended externally.
Arrives on removal by a moderator or a forced session end. The client must tear down the broadcast UI and stop capturing the microphone.
host_queue_pause_state
hostThe queue has been paused or resumed.
Payload:
{ paused: boolean }Hosts room only. Listeners see this state through sync.
jingles_updated
adminThe set of jingles has changed.
A signal to re-read the list. Jingles only exist in the cloud + sql + stream configuration.
library_updated
adminThe library changed: a track was added, edited, moved or deleted.
Payload: no data — it is only a signal to re-read
Broadcast to everyone, not just admins. The signal does not say what changed, so simply re-request the list you need — /api/admin/songs for an admin UI or /api/library for a listener. Emitted from several places: after an upload, after saving in the editor, after a mode change and after bulk operations.
listener_uid
listenerThe reply to listener_init: listener identity and art token.
Payload:
{
uid: '…', // derived from the IP; the client does not choose it
artToken: '…',
artTokenExpiresIn: 3600, // seconds
cooldownSecsLeft: 0, // until the next song request is allowed
}Listeners behind a shared NAT receive the same uid, and therefore share a token and a song-request cooldown.
live_hosts_roster
hostWho is currently on air: hosts and guests.
Sent to the hosts room on every change of line-up. Ordinary listeners never receive it.
phrases_updated
adminThe set of phrases has changed.
A signal to re-read the list. Unlike jingles, there is no stream_phrase_start/stream_phrase_end: a phrase is mixed into the song rather than replacing it, so listener-facing track metadata never changes.
privileges_updated
adminThis admin's permissions have just changed.
Payload:
{ privileges: ['queue_manage', '…'], authorized: true }Sent to the admin whose permissions the super admin changed. This is the event that makes permission changes take effect without logging in again — rebuild your interface from the new list.
queue_updated
adminThe queue has changed.
A broadcast signal to re-read the queue via get_queue. Listeners still get the radio state itself through sync.
radio_hosts_mode
listenerWhether the server was built with live host support.
Payload: boolean
Sent immediately on connect, without a request.
radio_hosts_online
listenerWhether anyone is on air right now.
Payload: boolean
Only sent when radio_hosts_mode is enabled.
stream_chat_mode_end
listenerThe conversation ended and the queue resumes.
stream_chat_mode_start
listenerHosts paused the queue — conversation has begun.
Payload:
{ serverTs: … }Duplicates what sync shows as title: "Just chatting", but arrives instantly rather than within two seconds.
stream_jingle_end
listenerThe jingle has finished.
stream_jingle_start
listenerA jingle between songs has started.
Payload:
{ jingleId: '…', serverTs: … }Time to hide track metadata: what is playing is not a song.
stream_track_start
listenerA new track started in the shared stream.
Payload:
{ trackId: '…', duration: 214.5, serverTs: 1754899200000 }serverTs is the start moment on the server's clock. Combined with your own latency measurement via stream_ping this gives an accurate position within the track.
suggest_cooldown_update
listenerSeconds left before another song can be requested.
Payload: number — seconds, 0 when a request is allowed
Sent to every connection sharing the same uid, so multiple tabs show the same countdown.
suggestion_result
listenerThe admin's decision on your song request.
Payload:
{ accepted: true, auto: false, song: { title, artist, … } }auto: true means the request expired on a timer rather than being rejected by hand. Sent only to the socket that made the request.
suggestions_update
adminThe current list of listener song requests.
Payload:
[{ uid: '…', song: { id, title, artist }, addedAt: 1754899200000 }]Sent only to admins holding queue_manage — on connect, on every change to the list, and when the radio switches to day mode and all requests are cleared.
sync
listenerThe complete radio state. The core event of the protocol.
Payload:
{
track: 'day/artist - title.mp3', // null while the mode is switching
title: 'Title', artist: 'Artist', album: 'Album', year: 2020,
duration: 214.5,
seek: 87.2, // position as of serverTimeMs
isPlaying: true,
playlist: [ /* up to 10 upcoming tracks */ ],
currentIndex: 3, totalTracks: 42,
mode: 'day', // 'day' | 'night'
pendingModeSwitch: null,
skipCooldownSecsLeft: 0,
uiSettings: { /* branding */ },
serverTimeMs: 1754899200000,
dayStartHour: 6, nightStartHour: 0,
}Broadcast to everyone every 2 seconds, and once immediately on connect. While the server switches modes a shortened form arrives with isPreparing: true and track: null. When hosts pause the queue, title becomes "Just chatting" and artist goes empty.
usersUpdate
listenerThe list of active listeners, for the header roster.
Payload:
[{ name: 'Name', img: 'file.png', color: '#a3f01c' }]Listeners are anonymous: the server assigns each connection a random name from an "oligarchs" set and a random colour. The name arrives already localized.