Core SDK
Presence & typing
Room presence and typing indicators over WebSocket.
Presence and typing flow over the room WebSocket — no separate REST polling required.
Typing with useChat
useChat exposes setTyping(isTyping) which sends a typing frame to the room Durable Object:
const { typingUsers, setTyping } = useChat({ roomId, client });
<input
onFocus={() => setTyping(true)}
onBlur={() => setTyping(false)}
/>
<p>
{Object.entries(typingUsers)
.filter(([, active]) => active)
.map(([uid]) => uid)
.join(", ")}{" "}
typing…
</p>Presence members
On connect, the room broadcasts presence events with member lists. The room store tracks presenceMembers:
const snapshot = useFluxyRoomStoreState(store);
snapshot.presenceMembers.map((m) => m.userId);Pass optional profile data when joining via WebSocket query (presenceInfo JSON) for richer avatars in the dashboard.
Rooms with more than 250 unique users switch to aggregate presence: presenceKind: "aggregate", presenceCount, and a sample of presenceMembers (24). Check presenceKind before rendering a full roster.
Late-joiner derived state
Non-CRDT app state (score, turn, ETA) can live in a JSON bag on the room Durable Object. It is included on subscription_succeeded as derived so a late joiner paints before replaying the log. Replace the bag with setDerivedState from useChat (30/min, 16 KiB). Use Yjs storage when you need conflict-free document edits.
const { derivedState, setDerivedState } = useChat({ roomId, client });
setDerivedState({ score: 3, turn: "ada" });Live cursors (presence, not broadcast)
Pointers use WebSocket type: "cursor" (up to 600/min, no D1, no webhooks). Do not send pointers as client_event (10/min + webhooks).
const { liveCursors, sendCursor, connected } = useChat({ roomId, replay: "request" });
function onPointerMove(e: React.PointerEvent<HTMLDivElement>) {
const rect = e.currentTarget.getBoundingClientRect();
sendCursor({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
label: "Ada",
});
}Scaffold: npx @fluxy-chat/create-fluxy-chat@latest my-cursors --example live-cursors. UI: <Cursors /> (damped spring via stepSpring) / <Selections /> / <AvatarStack /> from @fluxy-chat/ui. prefers-reduced-motion snaps without spring.
Selections ride on presence_patch (same throttle as cursors, no D1, no webhooks):
const [, updateMyPresence] = useMyPresence(store);
updateMyPresence({ selection: { x: 10, y: 20, x2: 80, y2: 40 } });client_event / useBroadcastEvent is for sparse signals (client-ephemeral-* skips webhooks). Not 60 Hz pointers.
Intents
Typing intents support composing, away, viewing_thread — surfaced as typingIntents in the room store for advanced UX (thread focus indicators).
See Connection state for transport details.