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_idmust 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.
| Event | Meaning |
|---|---|
presence_state | Current presence snapshot for the channel |
presence_diff | Presence changes after the snapshot |
msg:new | A live message or one message from the catch-up window |
catch_up:truncated | More messages remain beyond this catch-up window |
unread_count | Current 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:
- Persist the greatest fully applied
seqfor each channel. - Subscribe to live
msg:newevents before starting recovery. - Join with the persisted sequence.
- Merge catch-up and live messages by message
id. - Render messages in
seqorder, not arrival order. - When catch-up is truncated, continue from
next_after_sequntil the gap is empty. - 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 joinsMessageStoreordering and duplicate suppressionCatchUpManagerfor reading the store’s latest sequence before a join- A
catch_up:truncatedevent
Two recovery steps are not automatic yet:
- The in-memory
MessageStoredoes 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
CatchUpManagerdoes 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.