Realtime collaboration

Clank includes dependency free primitives for live presence, cursors, selections, typing state, and ephemeral signals between people authorized to the same application room. The transport uses ordinary authenticated Fetch writes plus an SSE

4 min read676 wordsClank 0.13.0

Clank includes dependency-free primitives for live presence, cursors, selections, typing state, and ephemeral signals between people authorized to the same application room. The transport uses ordinary authenticated Fetch writes plus an SSE stream, so it works through the same Node adapter, cookies, origin policy, and rolling HTTP infrastructure as the rest of an app.

Collaboration is deliberately separate from persistent live queries:

  • backend mutations and SQLite live queries are the durable source of truth;
  • collaboration presence is short-lived, replaceable UI context;
  • signals announce an event but do not become an event log; and
  • MCP tools continue to operate on documented backend actions, not another person's cursor.

Add an authenticated room

Use the auth adapter to reuse Clank's current session and CSRF boundary. The one application rule you must provide is room authorization:

ts
import { createAuthCollaborationHub } from "@clank.run/framework/collaboration";

const collaboration = createAuthCollaborationHub(auth, {
  async authorizeRoom(session, { room, operation }) {
    // Recheck membership on every connect, stream, update, signal, and heartbeat.
    return await canAccessDocument(session.requireUser().id, room);
  },
  displayName: (user) => user.profile.name ?? "Collaborator"
});

async function handle(request: Request) {
  const url = new URL(request.url);
  if (url.pathname === "/__clank/collaboration") {
    return collaboration.handle(request);
  }
  return application.handle(request);
}

authorizeRoom is not optional in applications with object- or workspace-level access. A valid session proves who someone is; it does not prove they may observe a particular document. createAuthCollaborationHub resolves the current session on every request and delegates writes to the auth runtime's ordinary verifyCsrf() check.

For another identity system, use createCollaborationHub({ authorize, verifyCsrf }). Both callbacks are mandatory. The public principal contains a private stable id and a room-visible name; peers receive only a random connection-scoped participant ID.

Connect the browser

ts
import { createCollaborationClient } from "@clank.run/framework/collaboration";

const room = createCollaborationClient({
  room: documentId,
  csrfToken: () => boot.auth.csrfToken,
  initialPresence: {
    route: location.pathname,
    cursor: { x: 0, y: 0 },
    selection: null
  }
});

await room.connect();

canvas.addEventListener("pointermove", (event) => {
  void room.update({
    route: location.pathname,
    cursor: { x: event.clientX, y: event.clientY },
    selection: selectedId.value
  });
});

effect(() => {
  renderPresence(room.participants.value);
});

The reactive surface is:

ValueMeaning
stateidle, connecting, connected, reconnecting, closed, or error.
participantsImmutable current room participants and their complete presence objects.
lastEventLatest snapshot, join, presence, leave, or signal event.
errorLatest transport error, without changing the participant contract.

update() replaces this connection's entire presence object. Replacement avoids stale cursor or typing keys surviving after a UI mode changes. Keep a local presence signal when updates are assembled by multiple components.

Use a signal for transient notification:

ts
await room.signal("comment.created", { commentId });

Signals are delivered to currently connected room participants, including the sender, and are never retained. Persist the comment through a typed backend mutation first; use the signal only to guide immediate UI attention.

Protocol and lifecycle

The endpoint defaults to /__clank/collaboration:

  1. a CSRF-protected connect write creates a random connection and returns a complete snapshot;
  2. the client opens an authenticated SSE stream for that exact room and connection;
  3. presence writes replace bounded participant data;
  4. signals broadcast bounded ephemeral payloads;
  5. heartbeats keep the connection lease current; and
  6. abort, explicit disconnect, or idle expiry publishes a leave event.

Every event uses clank-collaboration/1 and a monotonically increasing in-room revision. A stream always begins with a full snapshot, so reconnect does not require replaying an unbounded event history. The browser client reconnects with bounded exponential backoff after proxy restarts or rolling deployment transitions.

Limits and privacy

Defaults are conservative and configurable:

LimitDefault
Active rooms1,000
Participants per room50
Connections per authenticated principal8
Presence JSON4 KiB
Signal JSON8 KiB
Presence/signals per connection240/minute
Idle lease45 seconds

Data is JSON-only, five levels deep, with bounded keys, arrays, strings, bodies, and SSE events. Origins are same-origin, cross-site Fetch Metadata is rejected, responses are no-store, mutation rates are bounded, and stream access rechecks both the principal and room.

Presence is visible to every authorized participant in the room. Do not put passwords, tokens, private drafts, email addresses, or hidden database fields in it. Connection IDs are opaque routing handles, not bearer credentials; the server still re-authenticates their owner on every request.

The built-in hub is intentionally process-memory state. It survives ordinary connection churn but not a process restart, which is correct for ephemeral presence. The client reconnects and sends a new snapshot. A deployment with multiple simultaneous app processes must provide sticky routing to one hub or a reviewed shared collaboration adapter; SQLite live data remains the durable shared state either way.

Call hub.diagnostics() for aggregate room, participant, and stream counts. It returns no room names, participant identities, presence, connection IDs, or signal payloads. Call hub.close() during graceful application shutdown.