API & protocol
REST for sessions and discovery, Socket.IO for state and audio. Machine-readable: /api/v1/openapi.json. Full realtime reference: PROTOCOL.md in the server's folder.
Authentication
SONORA signs in with POST /api/v1/auth/login and gets an access token (1 hour) and a refresh token (30 days, rotated on every use). It sends Authorization: Bearer <accessToken>. The website uses its sign-in cookie plus an X-CSRF-Token header. Each session hands out short-lived scoped tokens from POST /sessions/{id}/join: listener (15 min, anyone allowed to see it) or publisher (2 h, its DJs). Ending a session voids all its tokens.
REST endpoints /api/v1
Errors are {"error": {"code", "message"}} with a matching status. Lists use cursor and limit; nextCursor is null on the last page.
Realtime (Socket.IO on /socket.io/)
Listening
sub.join {token} → {session, epoch, snapshot, deltas[], telemetry[], seq, chat[], participants, me}
sub.audio {sessionId, on} start/stop receiving audio packets
sub.resync {sessionId} → a fresh {epoch, snapshot, deltas, telemetry, seq}
chat.send {sessionId, text} → {ok, id} | {error}
events: state.snapshot · state.delta · state.telemetry · audio (binary) · stream.epoch ·
session.status · session.ended · participants · chat · chat.delete · chat.moderation · history
Publishing (SONORA)
pub.hello {token, protocolVersion: 1, publisherEpoch, engineSampleRate, audio: {codec, channels, bitrate, frameMs}}
→ {ok, streamEpoch}
pub.snapshot {streamEpoch, seq, revision, t, state}
pub.delta {streamEpoch, seq, baseRevision, revision, t, changes: [[key, value], …]} → {ok} | {resync: true}
pub.telemetry {streamEpoch, t, d: [[pos, rate, playing] | null, …], m: {ch[], pk[L,R], rms[L,R], clip}}
pub.audio binary: header + 1–16 Opus packets, each stamped with its engine frame
pub.played {streamEpoch, deck, track: {title, artist, bpm, key}}
pub.stop {end}
t is SONORA's audio clock in engine frames (a safe integer; divide by engineSampleRate for seconds). Track positions (pos, cue points, loops) are seconds of the source file, a different timeline: pitch, scratching and loops make them run at other speeds. A delta whose seq isn't the next one, or whose baseRevision doesn't match, is refused with resync and SONORA sends a new snapshot; listeners that see a gap ask for sub.resync.
State keys
layout.decks 2 | 4
deckN.track {asset, gen, title, artist, duration, bpm, key, artwork} | {masked, gen} | null
deckN.status / .load empty | loading | ready | error · 0–1
deckN.playing .looping .quantize .slip .sync .keyLock .reverse .vinyl .master booleans
deckN.loop {start, end} seconds of the track | null
deckN.loopBeats .semitones .tempo (0–1 fader) .tempoRange (fraction) .bpm (track tempo here)
deckN.cue .hotcues[8] {at, color, name, loop} only when the DJ shares cues
deckN.grid {bpm, offset, markers} only when the DJ shares waveforms
chN.trim .eqHigh .eqMid .eqLow .filter .fader 0–1 positions (units below)
chN.killHigh .killMid .killLow .quickOn booleans · chN.quickType · chN.xf A|thru|B · chN.cue
mixer.crossfader .master (0–1) · mixer.curve · mixer.limiter
fxU.assign[4] .master .mode (drywet|dryplus) .mix .super · fxU.slotS {type, on, level, params[]}
Units, as SONORA's engine applies them: trim ±12 dB linear in dB around the centre; EQ: centre unity, right +12 dB, left falls to a full kill; filter: centre is off, left low-pass, right high-pass; tempo: 1 + (tempo − 0.5) × 2 × tempoRange; master level gain v^1.6.