Cloud Sync Protocol

April 5, 2026 · View on GitHub

Tid3's cloud sync is a lightweight WebSocket protocol for sharing playback state and sending remote commands across devices. The reference server (Cloudflare Worker) and reference client (Go terminal client) are just two examples — any device that speaks this protocol can join the same session.


How It Works

All devices for the same user share a single server-side session. The active playback device publishes its state; the server broadcasts it to every other device. Any device can also send commands to control playback on the active device, or transfer it entirely.

sequenceDiagram
    participant A as Device A (active)
    participant S as Server
    participant B as Device B (observer)

    Note over A,B: State sync
    A->>S: UPDATE_STATE
    S-->>B: SYNC_STATE

    Note over A,B: Remote control
    B->>S: COMMAND (PAUSE)
    S-->>A: COMMAND (PAUSE)
    A->>S: UPDATE_STATE (IsPlaying: false)
    S-->>B: SYNC_STATE

Connecting

wss://<server>/sync?userId=<userId>&deviceId=<deviceId>&deviceName=<deviceName>
ParameterDescription
userIdThe user's TIDAL user ID. All devices with the same value share the same session.
deviceIdA stable unique identifier for this device. Use a UUID and persist it between sessions.
deviceNameHuman-readable label shown in device lists. Must be URL-encoded.

On a successful connection the server immediately sends INIT.

Connection flow

sequenceDiagram
    participant C as New Device
    participant S as Server
    participant O as Other Devices

    C->>S: WebSocket connect (userId, deviceId, deviceName)
    S-->>C: INIT { state, devices[] }
    S-->>O: DEVICE_JOINED { device }

    loop Every 30s
        C->>S: PING
        S-->>C: PONG
    end

    C--xS: disconnect
    S-->>O: DEVICE_LEFT { deviceId }

Message Format

All messages are UTF-8 encoded JSON text frames. Every message has a type field.

{ "type": "MESSAGE_TYPE", ... }

Multi-frame WebSocket messages must be reassembled before parsing.


Server → Client Messages

INIT

Sent immediately after connection. Contains the full device list and the last known state so the new device can catch up without waiting for the next UPDATE_STATE.

{
  "type": "INIT",
  "state": <SyncState> | null,
  "devices": [ <DeviceInfo>, ... ]
}

state is null if no device has published state yet. devices includes the connecting device itself — filter by deviceId to exclude self.


SYNC_STATE

Broadcast to all devices except the sender when any device sends UPDATE_STATE.

{
  "type": "SYNC_STATE",
  "data": <SyncState>
}

DEVICE_JOINED

Broadcast when a new device connects.

{
  "type": "DEVICE_JOINED",
  "device": <DeviceInfo>
}

DEVICE_LEFT

Broadcast when a device disconnects.

{
  "type": "DEVICE_LEFT",
  "deviceId": "string"
}

COMMAND

A command routed from one device to another (or all). See Commands.

{
  "type": "COMMAND",
  "data": <SyncCommand>
}

PONG

Response to a PING keepalive.

{ "type": "PONG" }

Client → Server Messages

UPDATE_STATE

Publishes the sender's current playback state. The server persists it and broadcasts it as SYNC_STATE to all other devices.

{
  "type": "UPDATE_STATE",
  "data": <SyncState>
}

Send this on every meaningful state change: track change, play/pause, seek, volume adjustment, queue edit.


COMMAND

Sends a playback command. The server routes it to the target device (or all devices if TargetDeviceId is empty).

{
  "type": "COMMAND",
  "data": <SyncCommand>
}

PING

Keepalive. Recommended interval: 30 seconds. The server responds with PONG.

{ "type": "PING" }

Data Structures

SyncState

FieldTypeDescription
QueueTrack[]Ordered list of tracks in the current queue
CurrentIndexnumberIndex of the currently playing track, or -1
PositionSecondsnumberCurrent playback position in seconds
DurationSecondsnumberDuration of the current track in seconds
VolumenumberVolume level, 0.01.0
IsPlayingbooleanWhether audio is currently playing
ShufflebooleanWhether shuffle is active
Repeatnumber0 = Off · 1 = All · 2 = One
ActiveDeviceIdstringdeviceId of the device that owns this state
ActiveDeviceNamestringHuman-readable name of the active device
LastUpdatedstring (ISO 8601)Timestamp of the last update

Track

FieldTypeDescription
TitlestringTrack title
ArtiststringArtist name

The Tid3 Track model has additional fields (album, cover URL, IDs…). Implementations that don't need them can safely ignore unknown fields.

DeviceInfo

FieldTypeDescription
deviceIdstringStable unique identifier
deviceNamestringHuman-readable label

SyncCommand

FieldTypeDescription
TypestringOne of the command types below
ValuestringOptional parameter (see table)
TargetDeviceIdstringTarget device, or "" to broadcast to all

Commands

TypeValueDescription
PLAYResume playback
PAUSEPause playback
NEXTSkip to next track
PREVGo to previous track
SEEKPosition in seconds ("42.5")Seek to position
VOLUMELevel 0.01.0 ("0.75")Set volume
TRANSFERTransfer active playback to TargetDeviceId

Session Rules

  • All devices sharing the same userId are in the same session.
  • State is persisted server-side between connections — a reconnecting device receives the last state via INIT.
  • The server does not enforce which device is "active". Any device may call UPDATE_STATE. Passive clients (like a display or remote control) should not.
  • Avoid feedback loops: never respond to an incoming SYNC_STATE or INIT by sending UPDATE_STATE. Check ActiveDeviceId and skip updates that originated from your own device.

Implementing a Client

flowchart TD
    A([Connect]) --> B[Receive INIT\nload state + devices]
    B --> C{Incoming message}
    C -->|SYNC_STATE| D[Update UI]
    C -->|DEVICE_JOINED| E[Add to device list]
    C -->|DEVICE_LEFT| F[Remove from device list]
    C -->|COMMAND| G[Execute command]
    C -->|PONG| H[Reset ping timer]
    D --> C
    E --> C
    F --> C
    G --> I[Send UPDATE_STATE\nif state changed]
    I --> C
    H --> C

    J([User / playback event]) --> K[Send UPDATE_STATE]
    L([User sends command]) --> M[Send COMMAND]
    N([Ping timer]) --> O[Send PING]

Minimum requirements:

  1. Open a WebSocket with userId, deviceId, deviceName
  2. Handle INIT — seed state and device list
  3. Handle SYNC_STATE — update display
  4. Handle DEVICE_JOINED / DEVICE_LEFT — maintain device list
  5. Handle COMMAND — execute playback actions
  6. Send UPDATE_STATE when local playback state changes (active devices only)
  7. Send PING every ~30 seconds

See the reference implementations: