Hub events
The realtime hub is a SignalR hub at /hub/rooms. @dotwire/client wraps it, so you only need this page if you're writing your own client or debugging the wire protocol.
Connecting
- Transport: WebSockets only. Skip negotiation; there is no long-polling or SSE fallback.
- Protocol: the SignalR JSON hub protocol. MessagePack isn't available.
- Auth: pass the token as the
access_tokenquery parameter. Browsers can't set headers on a WebSocket. - Node role: served by
AllandGatewaynodes. - Keep-alive: the server pings every 30 seconds.
- Token expiry: the server closes the connection when the token expires. Reconnect with a fresh token.
SignalR stateful reconnect is turned off. After a reconnect, call Subscribe again for each room and fetch missed messages with GET /rooms/{roomId}/messages?afterSeq=<last seq>. See Ordering and gap-fill.
Client-to-server methods
| Method | Arguments | Behavior |
|---|---|---|
Subscribe | roomId (UUID) | Checks the role cross-check and room membership, then joins the room. Queues a presence join. Fails with Forbidden if either check fails. |
Unsubscribe | roomId | Leaves the room and queues a presence leave. |
Typing | roomId, isTyping (boolean) | Broadcasts a typing notification to the room. Ignored unless the connection is subscribed to the room. |
Membership is checked when you call Subscribe, not on each event. A revocation removes the subscription immediately.
Subscribe and Unsubscribe share the per-connection hub rate bucket. Over the limit, they fail with RateLimited.
Typing has its own cooldown: the server drops a true notification that arrives within 0.5 seconds of the last one for that room, and drops a false notification unless the connection was typing.
There is no hub method for sending messages. Send with POST /rooms/{roomId}/messages; see the HTTP API.
When the connection closes, the server queues a presence leave for every room it was subscribed to.
Server-to-client events
| Event | Payload | When |
|---|---|---|
ReceiveMessage | { roomId, seq, senderId, time, content } | A message was accepted in a subscribed room. Content is decrypted. |
UserTyping | { roomId, userId, isTyping } | A member started or stopped typing. |
PresenceUpdated | { roomId, joined, left } | Coalesced presence changes. joined and left are arrays of user ids. |
MessageRetracted | { roomId, seq } | A message was redacted. Remove it from your UI. |
MembershipRevoked | { roomId, userId } | This user was removed from the room, and the connection was unsubscribed from it. |
Each event has one argument, a JSON object with camelCase fields. seq is a number. time is an ISO 8601 timestamp.
ReceiveMessage is the only event that is also stored. Typing, presence, retraction, and revocation events are ephemeral: a client that misses them doesn't get them back.
Errors
Hub method failures arrive as SignalR errors with these messages:
| Message | Cause |
|---|---|
Unauthorized | The connection's token has no sub or dw:role. |
Forbidden | The role cross-check failed, or the user isn't a member of the room. |
RateLimited | The connection's hub bucket is empty. |
The server doesn't send detailed errors to clients.
SSE equivalents
The SSE endpoint carries the same events under lowercase names: message, typing, presence, retracted, and revoked. See Stream room events.