Realtime

WebSocket events, presence & ACK

usePubSub is the client half of the pub/sub feature. It opens a single WebSocket to the events endpoint, subscribes to the topics you name, and streams server events into an h-state store you read reactively — connection status and the event log live there. (Presence is not a distinct store slice: it arrives as ordinary events on the 'user-presence' topic, read via events / getEventsByTopic('user-presence').)

It is resilient by default: a heartbeat (an application-level ping/pong JSON message, not a protocol PING frame) keeps the socket warm, drops trigger exponential-backoff reconnection, and every delivered message is acknowledged back to the server so the ACK/redelivery guarantees on the backend actually hold.

Configure it on the backend through the pubsub config block; consume it on the frontend with this one hook. No manual socket lifecycle, no reconnection loops to write.

Hook configuration#

usePubSub(config) takes the identity and topics to subscribe to, plus optional resilience tuning. userId is required client-side — it scopes the connection and, in unauthenticated deployments, is enforced by the maxClientsPerUser guard on the server. When the backend authenticates the WebSocket handshake (config.authenticate, wired whenever an access-token secret is set), the identity is taken from the verified handshake token instead and this query param is ignored.

userIdstringRequired

The authenticated user the socket belongs to. Always sent as a query param. In deployments without server-side auth it identifies the connection for per-user client limits and targeted delivery; when the backend authenticates the WebSocket handshake (config.authenticate), identity is derived from the verified token's subject and this query param is ignored.

topicsstring[]Optional

Topics to subscribe to on connect. Changing this array re-subscribes automatically. '*' receives everything you're permitted to see.

Example: ["orders", "notifications:user-42"]

Default["*"]
wsUrlstringOptional

Override the WebSocket origin. When omitted the hook derives ws://wss:// from window.location, so same-origin deployments need no URL at all.

wsPathstringOptional

The subscribe path — must match the pubsub.wsPath configured on the backend.

Default"/api/events/subscribe"
autoReconnectbooleanOptional

Reconnect automatically after an unexpected close. An auth-rejection close (code 4001) is respected and does not retry.

Defaulttrue
maxReconnectAttemptsnumberOptional

How many backoff attempts before giving up and surfacing an error on the store.

Default10
reconnectBaseDelaynumberOptional

Base delay in ms for exponential backoff (base · 2^attempt), capped by reconnectMaxDelay.

Default1000
reconnectMaxDelaynumberOptional

Upper bound in ms on any single reconnect delay.

Default30000
heartbeatIntervalnumberOptional

How often (ms) to send an application-level ping message (JSON { type: 'ping' }, answered by a 'pong' — not a WebSocket PING control frame) so proxies and the server keep the socket alive.

Default30000
debugbooleanOptional

Log connection lifecycle and message handling to the console.

Defaultfalse

What the hook returns#

Everything you need to render realtime UI: live connection flags, the typed event list, presence/identity and imperative controls. Reads are reactive — when a new event arrives, components re-render.

components/LiveOrders.tsx
1const { isConnected, events, subscribe } = usePubSub({2  userId: user.id,3  topics: ["orders"],4});5 6// ACK is automatic — every event with a messageId is acknowledged7return (8  <section>9    <span data-live={isConnected}>{isConnected ? "live" : "offline"}</span>10    {events.map((e) => (11      <Event key={e.id} topic={e.topic} payload={e.data} />12    ))}13  </section>14);
isConnected / isConnectingbooleanOptional

Connection status flags. isConnecting covers both initial connect and reconnect attempts so you can show one 'linking…' state.

eventsPubSubEvent[]Optional

The received events (id, topic, data, timestamp, receivedAt, messageId, isRedelivery), capped by a fixed 100-event ring buffer so memory stays bounded. (PubSubConfig still exposes a maxEvents field with a '(default 100)' JSDoc, but usePubSub never reads it — it is accepted-but-ignored, so setting it has no effect; the buffer is always 100.)

clientId / subscribedTopicsstring | string[]Optional

The server-assigned client id and the topics the server confirms you're subscribed to.

error / reconnectAttemptError | numberOptional

The last connection error (or null) and the current backoff attempt count for diagnostics UI.

subscribe / unsubscribe(topics: string[]) => voidOptional

Adjust subscriptions on a live socket without reconnecting. subscribe REPLACES the entire subscribed set with the array you pass (it does not merge/add), so the working way to drop a topic is to call subscribe again with the reduced array. Note: unsubscribe is currently a server-side no-op — the backend's 'unsubscribe' handler only logs the request and never removes topics, so the client keeps receiving those events; use subscribe with a smaller array to narrow instead. The server caps how many topics one client may hold (pubsub.maxTopicsPerClient, default 64); topics beyond the cap are silently dropped (the first 64 are kept, de-duped, each clamped to 256 chars — the client is not notified), so the request still succeeds with a truncated topic set.

connect / disconnect / clearEvents / getEventsByTopicfunctionsOptional

Imperative controls: force a (re)connect, tear down cleanly, empty the buffer, or read just one topic's events. Note: connect() reopens the socket only while the connection is still ref-held (e.g. the component stays mounted); once disconnect() fully releases it (refCount → 0), connect() is a no-op — you must remount or change the config to re-establish the socket.

Related sections