room.sendMessage()
Signals, awareness, and messaging — without one pipeline doing everything.
Four first-class primitives on one shared WebSocket. Pick the right tool — don't overload chat with cursors or awareness with messages.
Fire-and-forget events — cursors, reactions, strokes. Not stored.
Last-write-wins per user — status, tool, viewport. Snapshot on join.
Text messages with delivery/read receipts and optional durable backends.
Peer or SFU-backed binary payloads — files, game state, custom sync.
Choose the right primitive
Four coordination paths — different semantics, one room.
All ride the same WebSocket signaling connection with typed protocol envelopes. Your product UX stays coherent; your backend stays auditable.
SharedSignal — broadcast or unicast, optional TTL, rate-limited.
Awareness — patch-merge blobs held in room memory until leave.
Chat — server relay + optional Postgres/Mongo/Redis with AES-256-GCM at rest.
Data channel — WebRTC datachannel with schema helpers in the browser SDK.
SharedSignals
Typed ephemeral events at cursor speed.
Define a SignalMap for compile-time payload typing. Broadcast to the room or unicast to one participant. Server validates, rate-limits, and fans out — but never stores.
- Permissions
- room:send-signal (broadcast) · room:send-private-message (unicast)
- Limits
- 120 signals/participant/minute · 8 KB payload max
- Hook
- onSharedSignal — allow-list, audit, or drop before relay
- Namespaced kinds cursor:move, myapp.annotation.add avoid collisions.
- TTL drops stale frames Server ignores signals that arrive too late for smooth UI.
- Echo to sender Clients filter own senderId when rendering remotes.
- Node workers too Service bots can send and receive signals in the same room.
Awareness
Per-participant state that survives disconnect — and syncs on join.
Each participant owns one mergeable state blob: status, active tool, viewport, display overrides. New joiners receive a full snapshot; updates broadcast as patches. When someone leaves, peers get a tombstone — no ghost cursors.
Unlike signals, awareness state is held server-side for the room lifetime.
setLocalState({ status: "away" }) without resending the whole blob.
Compile-time safety for tool, status, and custom product fields.
Prototype-pollution guard rejects dangerous keys server-side.
Decision guide
Signals vs awareness — quick reference.
Use SharedSignal when…
- ✓ Event is transient / fire-and-forget
- ✓ Per-event: click, stroke, reaction, tick
- ✓ High frequency (cursor at 30+ Hz)
- ✓ Unicast to one participant
Use Awareness when…
- ✗ Late-joiners must see current value
- ✗ Per-participant state: status, tool, selection
- ✗ State should survive brief disconnect
- ✗ Roster or presence indicators
Note
Chat & messaging
Room chat with receipts, replay, and durable storage options.
Chat uses chat.send / chat.message envelopes with optional delivery and read receipts. In-memory history (1,000 messages) supports reconnect replay; plug in MongoDB, Redis, or Postgres for horizontal scale and encryption at rest.
- Worker output AI transcription and assistant events arrive as chat with workerOutput metadata.
- Replay on reconnect Cursor-based messageReplayed after WebSocket resume.
- Dedup Retried sends keyed by clientMessageId.
delivered · read · reconcile on reconnect
Memory · Mongo · Redis · Postgres
AES-256-GCM at rest when key set
See it live
Runnable demos — two tabs, immediate proof.
The shared-signals example combines cursor tracking, emoji reactions, and awareness-driven roster state. Chat-room demo focuses on messaging and presence. Kitchen-sink exercises all primitives together with voice and video.
Collaborative pointer canvas — cursor:move at ~30 Hz with TTL, reactions, tool/status awareness.
Multi-user chat, presence, and receipts — minimal messaging-focused demo.
Full product demo — chat, data channel file send, signals panel, worker output stream.
shared-signals-awareness.md · chat-storage.md · signaling.md message types.
Coordinate your product UI — then add voice and AI in the same room.
Signals and chat are half the story. Pair this deck with voice/video and voice AI for the full TomatoRTC narrative.
- 1 Run shared-signals
Two tabs on localhost — cursors, reactions, awareness roster
- 2 Read the guide
SharedSignals & Awareness integration guide
- 3 Related decks
Voice & video · Voice AI · Overview