Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/calmerism/Tamed/llms.txt

Use this file to discover all available pages before exploring further.

Music Together is Tamed’s collaborative listening feature. It lets multiple Tamed users share the same playback queue and stay perfectly in sync, regardless of whether they are on the same local network or connecting through the internet. The host’s MusicService acts as the single source of truth; all participants receive the same queue, position, and play/pause state in real time. Under the hood, Together uses a Ktor WebSocket server (TogetherServer) on the host device and a Ktor WebSocket client (TogetherClient) on each participant’s device. Synchronisation messages are serialised with kotlinx.serialization.

Architecture Overview

TogetherServer

Runs inside MusicService on the host device. Opens a WebSocket endpoint at /together on a configurable local port. Manages participant connections, approval, and state broadcast.

TogetherClient

Connects to a session via a TogetherJoinInfo (WebSocket URL, session ID, session key). Runs on each participant device, including the host’s own client view.

TogetherOnlineApi

REST API wrapper for the Tamed relay server. Allows hosting and joining sessions without requiring the participants to be on the same local network.

TogetherOnlineEndpoint

Discovers the current relay server base URL from tamed.app, caches it for 6 hours, and constructs the correct WebSocket URL for remote connections.

Server Events

TogetherServer emits a TogetherServerEvent sealed interface to MusicService for every significant participant action:
sealed interface TogetherServerEvent {
    data class JoinRequested(val participant: TogetherParticipant)    : TogetherServerEvent
    data class ParticipantJoined(val participant: TogetherParticipant): TogetherServerEvent
    data class ParticipantLeft(val participantId: String, val reason: String?): TogetherServerEvent
    data class ControlRequested(val request: ControlRequest)          : TogetherServerEvent
    data class AddTrackRequested(val request: AddTrackRequest)        : TogetherServerEvent
    data class Error(val message: String, val throwable: Throwable?)  : TogetherServerEvent
}
  • JoinRequested is emitted when requireHostApprovalToJoin is enabled; the host must call approveParticipant() to accept or reject.
  • ControlRequested carries a ControlRequest message, which contains a ControlAction variant (Play, Pause, SkipNext, SkipPrevious, SeekTo, SeekToIndex, SeekToTrack, SetRepeatMode, SetShuffleEnabled) submitted by a guest.
  • AddTrackRequested carries an AddTrackRequest message, which contains the TogetherTrack to add and an AddTrackMode — either PLAY_NEXT or ADD_TO_QUEUE.

Session Data Model

data class TogetherRoomState(
    val sessionId: String,
    val hostId: String,
    val participants: List<TogetherParticipant>,
    val settings: TogetherRoomSettings,
    val queue: List<TogetherTrack>,
    val queueHash: String,
    val currentIndex: Int,
    val isPlaying: Boolean,
    val positionMs: Long,
    val repeatMode: Int,
    val shuffleEnabled: Boolean,
    val sentAtElapsedRealtimeMs: Long,
)
Each TogetherParticipant carries id, name, isHost, isPending, and isConnected fields. Pending participants receive a restricted RoomStateMessage with an empty queue until approved. A TogetherJoinInfo is serialised into a deep link for sharing:
tamed://together?host=<host>&port=<port>&sid=<sessionId>&key=<sessionKey>
TogetherLink.encode() / TogetherLink.decode() handle serialisation and deserialisation. Three link formats are supported:
  1. Deep link (tamed://together?…) — the standard shareable format.
  2. WebSocket URL (ws://host:port/together?sid=…&key=…) — useful for manual entry.
  3. Compact string (host|port|sessionId|sessionKey) — minimal pipe-separated format.

Host Configuration

All host settings are persisted in DataStore and applied when starting a session:
KeyTypeDescription
TogetherDisplayNameKeyStringThe name shown to participants for the host
TogetherClientIdKeyStringA stable UUID identifying this device across sessions
TogetherDefaultPortKeyIntLocal port the WebSocket server listens on
TogetherAllowGuestsToAddTracksKeyBooleanWhether guests can submit AddTrackRequest
TogetherAllowGuestsToControlPlaybackKeyBooleanWhether guests can send ControlRequest
TogetherRequireHostApprovalToJoinKeyBooleanHold new joiners in a pending state until approved
These map directly to the TogetherRoomSettings data class:
data class TogetherRoomSettings(
    val allowGuestsToAddTracks: Boolean = true,
    val allowGuestsToControlPlayback: Boolean = false,
    val requireHostApprovalToJoin: Boolean = false,
)

Session State Machine

TogetherSessionState is a sealed class flowing from MusicService:
StateDescription
IdleNo active session
HostingLocal server running; join link available
HostingOnlineSession relayed through the Tamed online server
JoiningClient connecting to a local host
JoiningOnlineClient connecting via online relay code
JoinedConnected; role is either Host or Guest
ErrorConnection failed or session terminated unexpectedly

How to Host a Session

1

Open Music Together

Navigate to Now Playing → Together or find Together in the main menu. The welcome screen appears on first use (TogetherWelcomeShownKey tracks whether it has been shown).
2

Configure your display name

Set your display name in Settings → Together → Display Name (TogetherDisplayNameKey). This is shown to participants in the listener list.
3

Choose hosting mode

Select Local to host directly from your device (participants must be on the same network or reach your IP) or Online to use the Tamed relay server for internet-accessible sessions.
4

Configure room settings

Toggle whether guests can add tracks (TogetherAllowGuestsToAddTracksKey), control playback (TogetherAllowGuestsToControlPlaybackKey), and whether new joiners need your approval (TogetherRequireHostApprovalToJoinKey).
5

Start the session

Tap Start Session. Tamed starts TogetherServer (or calls TogetherOnlineApi.createSession() for online mode) and generates a join link. Share the link with friends.
6

Approve pending participants (optional)

If host approval is required, each JoinRequested event shows a prompt. Approve or reject each participant individually.

How to Join a Session

1

Receive the join link

Ask the host to share the join link. It will look like tamed://together?host=… or a short invite code if the session is online.
2

Tap the link or enter manually

Tapping the deep link opens Tamed directly into the join flow. Alternatively, open Music Together → Join and paste the link. The last used link is remembered in TogetherLastJoinLinkKey.
3

Enter your display name

Type the name you want other participants to see. Tamed uses your TogetherDisplayNameKey as the default.
4

Connect

TogetherClient.connect() opens a WebSocket connection, sends a ClientHello handshake with the session ID, session key, client ID, and display name, then listens for a ServerWelcome response.
5

Wait for approval (if required)

If the host has enabled approval, your client receives a JoinDecision message. Once approved, the full RoomStateMessage (queue, position, playback state) is delivered and playback syncs automatically.

Security

Every session has two secrets generated at creation time:
  • Session ID — identifies the session (shared publicly in the join link).
  • Session Key — a shared secret that authenticates every client joining the session. Any client presenting the wrong key receives an Invalid session error and is disconnected.
For online sessions, the Tamed relay server issues separate hostKey and guestKey values. Guests receive only the guestKey; the host retains the hostKey for administrative control.
When hosting locally, your device’s IP address is embedded in the join link. Only share the link with people you trust, as it allows anyone with it to attempt to join your session (subject to approval settings).

Build docs developers (and LLMs) love