Search the complete documentation

Try idempotency, proposal lifecycle, authentication, or ordering.

Navigate Open
繁體中文
Browse documentation

Relay · Realtime

Realtime events and reconnect recovery

The WebSocket carries live conversation events. Channel-local seq remains the source of truth when events arrive late, repeat after reconnect, or mix with REST catch-up results.

Connect

Connect to the Phoenix socket endpoint and pass the same host-signed JWT used by the REST API:

import { Socket } from "phoenix";

const socket = new Socket("wss://relay.example.com/socket", {
  params: { token: relayToken }
});

socket.connect();

The server authenticates the token before a client can join any channel.

Join a channel

The topic contains the signed tenant ID and the channel ID:

tonetify-relay:<tenant_id>:<channel_id>

Pass the last sequence already applied by the client:

const channel = socket.channel(
  `tonetify-relay:${tenantId}:${channelId}`,
  { last_seq: lastAppliedSeq }
);

channel.join();

Relay checks two conditions at join time:

  • The topic tenant must equal the token’s tenant_id.
  • The token’s user_id must be a channel member.

A failed membership or tenant check returns not_a_member. A join that exceeds the configured frame budget returns rate_limited.

Events after join

After a successful join, Relay sends the current presence state, any requested catch-up messages, and the channel’s unread count.

EventMeaning
presence_stateCurrent presence snapshot for the channel
presence_diffPresence changes after the snapshot
msg:newA live message or one message from the catch-up window
catch_up:truncatedMore messages remain beyond this catch-up window
unread_countCurrent unread count for this user and channel

These five events form the stable realtime core for contract beta. Edit, retract, pin, reaction, thread, and typing events are preview surfaces. They exist in the implementation and SDK, but their names, payloads, and recovery behavior are not yet covered by the public compatibility policy.

Catch-up window

When last_seq is present, Relay reads messages with a greater sequence and pushes them as msg:new in ascending order. The default WebSocket catch-up window is 200 messages.

If more messages remain, Relay sends this event after the current batch:

{
  "channel_id": "channel-uuid",
  "after_seq": 42,
  "next_after_seq": 242,
  "limit": 200
}

catch_up:truncated means the batch ended at next_after_seq. It does not mean Relay lost messages.

Continue from that sequence by rejoining with a new last_seq, or use REST:

GET /api/channels/{channel_id}/messages?after_seq=242&limit=100
Authorization: Bearer <jwt>

REST returns at most 100 messages per request. Continue until a response contains fewer than 100 messages.

Reconnect algorithm

A reliable client follows this order:

  1. Persist the greatest fully applied seq for each channel.
  2. Subscribe to live msg:new events before starting recovery.
  3. Join with the persisted sequence.
  4. Merge catch-up and live messages by message id.
  5. Render messages in seq order, not arrival order.
  6. When catch-up is truncated, continue from next_after_seq until the gap is empty.
  7. Advance the persisted sequence only after the message has been accepted into the local store.

Catch-up messages may repeat messages already present locally. Duplicate delivery is expected during recovery; the stable message id and seq make the merge deterministic.

Sending during a disconnect

A send timeout leaves the write result unknown. Keep the original nonce and retry after the connection returns. Relay responds with the existing message if the first attempt committed.

Do not create a new nonce merely because the transport disconnected. A new nonce means a new logical message.

Current SDK boundary

The current @tonetify/relay provides:

  • Socket reconnection with exponential backoff and jitter
  • A 30-second default heartbeat
  • channel.join(lastSeq) for catch-up joins
  • MessageStore ordering and duplicate suppression
  • CatchUpManager for reading the store’s latest sequence before a join
  • A catch_up:truncated event

Two recovery steps are not automatic yet:

  • The in-memory MessageStore does not persist the last sequence across a page reload or process restart.
  • Automatic socket rejoin does not advance its join parameter to the newest applied sequence, and CatchUpManager does not drain truncated windows.

Until those gaps are closed, the host should persist last_seq and run REST catch-up when the socket reconnects or the application becomes active again. Merge REST and WebSocket messages by id, then order them by seq.

This is an SDK productization gap. The server reconnect contract already supports complete recovery.

Next: understand channel lifecycle and permissions.