Advanced Features
Audience: users.
WebRTC with Relay Signaling (Available Baseline)
Section titled “WebRTC with Relay Signaling (Available Baseline)”import { createRoom } from '@roomful/core';
const room = createRoom('doc-room', { transport: 'webrtc', relayUrl: 'ws://localhost:8787', relayAuth: async () => { const res = await fetch('/api/roomful-token'); const body = await res.json(); return body.token; }, stunUrls: ['stun:stun.l.google.com:19302'], webrtc: { iceGatherTimeoutMs: 5000, dataChannel: { ordered: true, protocol: 'roomful-v1' }, },});When relayAuth is configured, Roomful appends the resolved token to the relay WebSocket URL as ?token=... and keeps the join payload token-free.
End-to-End Encryption
Section titled “End-to-End Encryption”const room = createRoom('secure-room', { encryption: { passphrase: 'replace-with-secure-secret', },});Security notes:
- Distribute keys/passphrases out-of-band.
- Never hardcode production secrets in frontend code.
encryption: { key }also accepts a pre-derived AES-GCMCryptoKey.- Passphrase mode derives a non-extractable AES-GCM key with PBKDF2-SHA-256 and room-scoped salt context.
helloandwelcomeremain plaintext control messages so peers can negotiate encryption capability before exchanging room payloads.- Presence, state, events, awareness, and CRDT sync payloads are encrypted with AES-GCM before reaching the transport layer.
- Relay servers only see routing metadata plus ciphertext and cannot inspect decrypted application payloads.
- Wrong keys or tampered payloads fail gracefully with
DECRYPTION_ERROR.
Relay Signaling Server (@roomful/relay)
Section titled “Relay Signaling Server (@roomful/relay)”import { createRelayServer, verifyJWT } from '@roomful/relay';
const relay = createRelayServer({ port: 8787, maxConnections: 1000, redisUrl: process.env.ROOMFUL_REDIS_URL,}).auth(async (peerId, roomId, token) => { const claims = verifyJWT(token, process.env.RELAY_JWT_SECRET ?? ''); if (claims.roomId !== roomId) { throw new Error(`Token cannot join room ${roomId}.`); }});
await relay.start();CLI startup:
roomful-relay --host 0.0.0.0 --port 8787 --max-connections 1000curl http://127.0.0.1:8787/healthHorizontal scaling with Redis:
ROOMFUL_REDIS_URL=redis://127.0.0.1:6379/0 \roomful-relay --host 0.0.0.0 --port 8787 --max-connections 1000The relay package is the self-hostable baseline for both:
- WebRTC SDP/ICE signaling
- WebSocket room message relay
- shared-port health checks at
GET /health
Relay runtime defaults and knobs:
HOST: default127.0.0.1PORT(orROOMFUL_PORT): default8787;ROOMFUL_PORTtakes precedence overPORTMAX_CONNECTIONS: optional global concurrent WebSocket cap per relay instanceROOMFUL_MAX_ROOM_SIZE: optional hard per-room peer capROOMFUL_CORS_ORIGIN: optional allowed browser origin; adds CORS headers on HTTP responses and rejects WebSocket upgrades from other origins (use*to allow any origin)ROOMFUL_AUTH_SECRET: enables built-in HS256 JWT authorization; peers must present a valid token signed with this secretROOMFUL_REDIS_URL: optional Redis connection string; when set, relay room coordination switches to multi-instance mode automatically- relay auth is disabled by default; unconfigured relays remain open
- when auth is enabled, clients must connect with a single non-empty
tokenquery param - invalid upgrade-stage tokens are rejected with HTTP
401 - join-time auth failures emit
AUTH_FAILEDand close the socket with code4401 - join attempts emit
REDIS_UNAVAILABLEwhen Redis-backed coordination is configured but not currently ready
Docker runtime:
docker pull roomful/relay:latestdocker run --rm -p 8787:8787 -e HOST=0.0.0.0 roomful/relay:latestRedis-backed relay behavior:
- room membership is shared across relay instances
- peer join/leave notifications are forwarded through Redis room channels
- direct relay signaling and websocket transport frames are forwarded across instances
- existing same-instance sockets remain connected during transient Redis loss, but new joins are rejected until coordination recovers
WebSocket relay example:
const room = createRoom('doc-room', { transport: 'websocket', relayUrl: 'ws://localhost:8787', websocket: { fallbackTransport: 'polling', },});Polling fallback notes:
- fallback is opt-in on
transport: 'websocket' - fallback only applies during the initial connect/reconnect attempt when WebSocket is blocked or unavailable
- once fallback activates, that room instance stays on polling until you call
disconnect() autoselection order is unchanged; polling is not a public transport mode
Reconnection
Section titled “Reconnection”const room = createRoom('my-room', { reconnect: { maxAttempts: 10, backoffMs: 500, backoffMultiplier: 1.5, maxBackoffMs: 30000, },});Automatic reconnection is opt-in. Set reconnect: true to use the default strategy, or pass an object to override:
maxAttempts: default5backoffMs: default100backoffMultiplier: default2maxBackoffMs: default2000
Behavior:
- unexpected transport disconnects begin retrying within
500ms - retries use exponential backoff with internal jitter
reconnectingfires for each retry attempt- successful recovery emits
connectedagain disconnectedis deferred until retry exhaustion when auto reconnect is enabled- room identity and local engine state are preserved across reconnect attempts
CRDT with Yjs
Section titled “CRDT with Yjs”Roomful ships a room-scoped Yjs document and provider:
-
install
yjsandy-protocolsalongside@roomful/core -
@roomful/coreexposes them as peer dependencies because CRDT support is an advanced integration surface -
room.getYDoc()returns the sharedY.Doc -
room.getYProvider()returns the shared provider withdoc,awareness,status, andsynced -
new peers bootstrap document state via Yjs state-vector exchange on the existing Roomful transport
-
CRDT wire payloads stay binary in-process and use the negotiated transport codec, with JSON-safe array fallback when needed
Use this for collaborative editors and other conflict-free shared structures:
room.getYDoc().getText('content')for text-centric editorsroom.getYDoc().getXmlFragment('prosemirror')for ProseMirror-style document treesroom.getYProvider().awarenessfor cursor, selection, and collaborator metadata
Auth Pattern
Section titled “Auth Pattern”For private rooms in relay mode:
- validate
tokenserver-side withrelay.auth(...) - use
verifyJWT(token, secret)for HS256 JWTs when you want a built-in helper - map identity to peer metadata
- reject unauthorized joins before admission
- leave auth disabled for open rooms