Robutler

host.collab

host.collab is the App SDK surface for realtime multi-user rooms backed by the Hocuspocus collab pod and Yjs CRDTs. The host checks the workspace ACL, then mints a short-lived JWT your app uses to dial the pod with the Yjs provider of your choice (@hocuspocus/provider). The host does not bundle a Y.Doc for you: you bring your own CRDT runtime and open the socket with the token.

Collab is gated by the manifest collab flag. Only an app whose widget.json declares collab: true may mint a room-scope (code-based lobby) token, and the registry uses that flag to authorize the mint. See the manifest reference.

API

interface CollabNamespace {
  getToken(scope: 'workspace' | 'item' | 'room', scopeId: string): Promise<CollabTokenResult>;
  getTurnCredentials(): Promise<{ turn: TurnCredentials | null }>;
  room(contentId: string): CollabRoomApi;
}

interface CollabTokenResult {
  token: string;          // pass to new HocuspocusProvider({ token })
  wsUrl: string;          // absolute wss:// URL to dial
  roomId: string;         // Hocuspocus document name: new HocuspocusProvider({ name: roomId })
  expiresAt: string;      // ISO-8601 token expiry
  identity: ParticipantIdentity; // write into awareness.setLocalStateField('user', identity)
}

interface ParticipantIdentity { id: string; displayName: string; color: string }

interface CollabRoomApi {
  generateCode(len?: number): string;          // fresh shareable code (default len 6, charset A-Z/2-9)
  join(code: string): Promise<CollabTokenResult>; // token for the <contentId>:<code> lobby
}

interface TurnCredentials {
  username: string;
  credential: string;
  urls: string[];   // pass straight into RTCIceServer.urls
  expiresAt: number; // wall-clock unix-seconds
}

Room scopes

getToken(scope, scopeId) keys off one of three scopes:

ScopescopeIdUse
workspacea workspace idWorkspace-wide presence and shared cursors across the canvas.
itema workspace item idA per-item Yjs subdoc, one room per canvas item.
room<contentId>:<roomCode>A code-based lobby under a collab-enabled app's content id.

The document name on the wire is the returned roomId.

Join a room

import { HocuspocusProvider } from 'https://esm.sh/@hocuspocus/provider';
import * as Y from 'https://esm.sh/yjs';

await host.ready();

const { token, wsUrl, roomId, identity } = await host.collab.getToken(
  'item',
  host.workspace.itemId,
);

const ydoc = new Y.Doc();
const provider = new HocuspocusProvider({ url: wsUrl, name: roomId, token, document: ydoc });

// Identify yourself in awareness so peers see your name + cursor color.
provider.awareness.setLocalStateField('user', identity);

Tokens are short-lived (expiresAt). Re-mint when you reconnect after an expiry.

Code-based lobby rooms

host.collab.room(contentId) is sugar for shareable lobby rooms under a collab-enabled app. Generate a code, share it, and join:

const lobby = host.collab.room(myContentId);
const code = lobby.generateCode();          // e.g. 'K7P2QX'
const { token, wsUrl, roomId } = await lobby.join(code); // code is uppercased

join(code) mints a token for the <contentId>:<code> room. This path is what the collab manifest flag gates: a room token cannot be used to reach arbitrary item or workspace rooms.

TURN credentials for WebRTC

If your app does WebRTC (a video huddle, a voice call), call getTurnCredentials only when the call actually starts, not on mount. It triggers a TURN-provider round-trip, so it is deliberately not bundled into getToken (which runs on every collab mount).

const { turn } = await host.collab.getTurnCredentials();

const pc = new RTCPeerConnection({
  iceServers: turn
    ? [{ urls: turn.urls, username: turn.username, credential: turn.credential }]
    : [], // null → no relay provider configured; run STUN-only
});

turn is null when no relay provider is configured; fall back to STUN-only in that case.

Reserved awareness namespaces

When you publish awareness updates, do not spoof host-owned keys. The collab pod drops inbound updates from app origins that touch:

  • presence.*: canvas cursors and follow-me (host chrome)
  • comment.*: thread typing indicators
  • webrtc.*: multi-party RTC signaling
  • user: the top-level identity field (set it only with the identity from getToken)

Errors

  • service_unavailable: the collab pod or Redis is briefly unavailable; retry with backoff.
  • ACL failures and token-reuse replay rejections surface per the error codes.

On this page