This document outlines the strategy for adding real-time, ephemeral messaging (cursors, presence, live reactions) to Ratatoskr using automerge-repo's native ephemeral message type.
Instead of writing high-frequency data (like mouse positions) to the CRDT history—which causes bloat and performance issues—we will use the ephemeral message type. These messages are fire-and-forget, routed via the existing WebSocket connection, and scoped to a specific documentId.
The Server acts as a Relay/Router. It does not store these messages.
- Client A sends an ephemeral message for
doc-123. - Server verifies Client A has
READpermission fordoc-123. - Server looks up which other clients are currently "watching"
doc-123. - Server forwards the message to those clients.
We need to ensure every peer has a displayable identity.
- Verified Users: Use the
namefrom their account. - Anonymous Users: The server assigns a random "fun" name upon connection.
- Format:
Adjective Animal(e.g., "Neon Narwhal", "Curious Capybara"). - Store this in the
AuthenticatedClientobject.
- Format:
To ensure identity cannot be forged:
- Broadcast Identity: When a client authenticates (
auth_ok), the server broadcasts atype: "peer_identity"message to all clients:{ peerId: "...", user: { id: "...", name: "...", isAnonymous: boolean } }. - Initial Sync: When a new client joins, send them the identities of all currently connected peers.
- Cleanup: Broadcast
peer_disconnectedwhen a socket closes.
To support reactive UI (e.g., "User X left the document" even if they are still online), we use an Explicit Presence Protocol.
-
Protocol: Clients send ephemeral messages to signal status changes.
{ type: "presence", status: "join", documentId: "..." }{ type: "presence", status: "leave", documentId: "..." }{ type: "presence", status: "heartbeat", documentId: "..." }(every 30s)
-
Server Logic:
- Maintain
documentPresence: Map<DocId, Set<PeerId>>. - On "join": Add peer to map. Broadcast
type: "peer_joined_doc", documentId, peerId. Sendtype: "doc_presence_state"to the joining peer. - On "leave": Remove peer from map. Broadcast
type: "peer_left_doc", documentId, peerId. - On Disconnect: Find all docs where this peer was present. For each, remove them and broadcast "peer_left_doc".
- Maintain
Intercept messages with type: "ephemeral".
- Check Permissions: Use
canReadDocument(docId, client.userId). - Rate Limiting: Enforce limits (e.g., 600/min) using
checkRateLimit. - Broadcast: Forward to all subscribers of the
documentIdexcept the sender.
The RatatoskrClient should expose a reactive store:
// Subscribe to presence changes
ratatoskr.on("presence-changed", (docId, peers) => {
console.log(`Users viewing ${docId}:`, peers);
});
// Explicitly enter/leave (e.g., on mount/unmount)
ratatoskr.enterDocument(docId);
ratatoskr.leaveDocument(docId);If readers need to "write" to a channel (e.g., a public chat for a private doc), use a Sidecar Channel.
- Strategy: Send messages on a separate ID like
eph:chat-<docId>. - Result: The
eph:prefix allows public relaying while the maindoc:remains protected.
- Zero Storage Cost: Messages are never saved to the DB.
- Reactive: UI updates instantly when users join, leave, or close tabs.
- Secure & Verified: Reuses existing document ACLs and server-verified identities.
- Social: Immediate sense of "others are here" even for anonymous users.