Yama // API · Real-time

Real-time (WebSocket) guide

How a client receives live chat events over a single WebSocket, and the full event catalogue.

Connecting

Yama exposes one WebSocket endpoint (Rails Action Cable) at /cable. You authenticate with the same user JWT as the REST API, passed as a token query parameter (a browser cannot set headers on a WebSocket, so the token goes in the URL):

wss://yama.modulotech.fr/cable?token=<jwt>
Base URL — Production: wss://yama.modulotech.fr/cable · Staging: wss://staging.yama.modulotech.fr/cable. Always over wss:// (TLS). An invalid, expired or revoked token (after logout) is refused at the handshake.

Subscribing

After the handshake, subscribe to a single channel — UserChannel. There are no parameters: the server already knows who you are from the token, and pushes every event relevant to you onto this one stream (messages from your rooms, status changes, unread counts, mutes, membership changes, read receipts). Using the Action Cable protocol directly:

// 1. subscribe
ws.send(JSON.stringify({
  command: "subscribe",
  identifier: JSON.stringify({ channel: "UserChannel" })
}));

// 2. every event arrives as a "message" frame; the payload is in .message
ws.onmessage = (e) => {
  const frame = JSON.parse(e.data);
  if (frame.type) return;                 // ping / confirm / reject envelopes
  const event = frame.message;            // { "type": "...", ... }
  switch (event.type) {
    case "message_created": /* ... */ break;
    case "unread_updated":  /* ... */ break;
    // ...
  }
};

With a native Action Cable client (e.g. the actioncable JS package or a Swift/Kotlin port), just point the consumer at the URL above and subscribe to { channel: "UserChannel" }; each event is delivered to your received(data) callback.

Event envelope

Every event is a JSON object with a type field, event-specific fields, and a per-recipient muted boolean. Treat unknown types as no-ops (forward-compatible).

muted (on every event) is computed for you: true when your account is muted, or — for a room-scoped event — that room is muted for you. Use it to decide whether to surface a notification (banner/sound). State-sync events (status, membership, unread…) carry it too for consistency; you can ignore it there.

Events

message_created

A new message was posted in a room you belong to (you also receive your own, for multi-device sync). The muted flag is computed for you: it is true when this room is muted for you or your account is muted — i.e. don't surface a notification (banner/sound) when true. The unread count still updates via a separate unread_updated.

{
  "type": "message_created",
  "muted": false,                // true -> suppress the notification for this recipient
  "message": {
    "id": "uuid", "room_id": "uuid", "user_id": "uuid",
    "content": "Hello", "created_at": "2026-06-19T10:00:00Z",
    "file": { "filename": "photo.jpg", "url": "/rails/active_storage/..." } // or null
  }
}

message_read

A room member (possibly you, on another device) read messages up to a given one. Use it for "seen" indicators. The reader's own unread count is refreshed via a separate unread_updated.

{
  "type": "message_read",
  "room_id": "uuid",
  "up_to_message_id": "uuid",   // this and all earlier messages are read
  "user_id": "uuid",            // who read
  "read_at": "2026-06-19T10:01:00Z",
  "muted": false
}

unread_updated

Your unread count for a room changed (a message arrived, or you read/​unread some). Authoritative — replace your local counter with this value.

{ "type": "unread_updated", "room_id": "uuid", "unread_count": 3, "muted": false }

status_changed

A user you share a room with (or you, on another device) changed availability. status is one of connected, disconnected, unknown, muted.

{ "type": "status_changed", "user_id": "uuid", "status": "connected", "muted": false }

muted here reflects your account mute only (this event is not tied to a room).

room_muted / room_unmuted

You changed the per-room mute (e.g. from another device). muted_until is the expiry of a timed mute, or null when unmuted or muted with no end.

{ "type": "room_muted",   "room_id": "uuid", "muted": true,  "muted_until": "2026-06-19T12:00:00Z" }
{ "type": "room_unmuted", "room_id": "uuid", "muted": false, "muted_until": null }

Note: the account-wide mute is conveyed as a status_changed with status: "muted", not as a room event.

room_added

You were added to a room (you joined, or an admin added you). Add it to your room list.

{ "type": "room_added", "room": { "id": "uuid", "name": "Support", "slug": "support" }, "muted": false }

room_removed

You were removed from a room (you left, or an admin removed you). Drop it from your room list.

{ "type": "room_removed", "room_id": "uuid", "muted": false }

member_joined / member_left

Another user joined or left a room you belong to. Update that room's member list / presence.

{ "type": "member_joined", "room_id": "uuid", "user_id": "uuid", "muted": false }
{ "type": "member_left",   "room_id": "uuid", "user_id": "uuid", "muted": false }

Event summary

typewhenwho receives it
message_createdmessage postedall room members
message_readmember reads messagesall room members
unread_updatedyour unread count changesyou
status_changedavailability/account-mute changesyou + room co-members
room_muted / room_unmutedyou mute/unmute a roomyou (all devices)
room_addedyou are added to a roomyou
room_removedyou are removed from a roomyou
member_joined / member_leftanother user joins/leaves your roomother room members

Reliability notes