Pub/Sub

WebSocket + Dapr realtime messaging

The pubsub block powers realtime: it lets your backend push events to connected clients over WebSocket, and (under Dapr) fan messages out across service instances via a pub/sub component. It's what drives live notifications, presence and any streaming UI.

Reliability features are built in — an ACK system retries unacknowledged messages, presence broadcasting announces who's online, and a cleanup loop reaps stale connections so resources don't leak.

Core#

The connection surface and limits. Defaults are production-ready; override paths only to avoid collisions.

enabledbooleanOptional

Master switch for the pub/sub subsystem. Requires Redis to be configured — the whole subsystem only registers when a Redis manager exists; enabling pub/sub without Redis silently disables it (only a startup warning, 'pubsub is enabled but Redis is not configured. Disabling PubSub.', is logged — no error).

Defaultfalse
basePathstringOptional

Base path for Dapr subscription endpoints (the routes Dapr posts delivered messages to).

Default"/subs"
wsPathstringOptional

The WebSocket endpoint clients connect to for the realtime feed. The handshake is authenticated whenever an access-token secret is configured: identity (userId) is derived from the verified token and an unauthenticated handshake is closed with code 4001 ('Unauthorized'). Absent an access-token secret, authentication is skipped and identity falls back to the client-supplied userId query param.

Default"/api/events/subscribe"
pubsubNamestringOptional

Intended as the name of the Dapr pub/sub component used to publish and subscribe across instances. This is the real config-level default, but the value is currently accepted and stored yet never consumed by the pub/sub routes — it has no runtime effect and does not select the component used by the Dapr publish path (DaprPubSubClient.publish / publishBulk both default to DEFAULT_PUBSUB_NAME, 'pubsub-rabbitmq').

Default"pubsub-redis"
maxClientsPerUsernumberOptional

Cap on simultaneous WebSocket connections per user — prevents a single account exhausting sockets. Past the limit the user's oldest connection is evicted, not the new one rejected.

Default10
maxTopicsPerClientnumberOptional

Max distinct topics a single client may subscribe to. Bounds the in-memory subscription set against an attacker-controlled topics array (memory-exhaustion DoS); enforced on both the handshake and every subscribe message, with each topic string also clamped to 256 chars and de-duplicated. Excess topics are silently dropped.

Default64
daprApiTokenstringOptional

Shared secret that gates the POST {basePath}/:topic Dapr subscription (ingress) endpoint. When set, delivered events must present a matching dapr-api-token header (constant-time compare) or get HTTP 403; falls back to env APP_API_TOKEN, then DAPR_API_TOKEN. If unset the endpoint is unauthenticated (one warning is logged on the first unauthenticated inbound event, not at startup) and anyone reaching it can inject realtime events to any user — set it wherever the route is network-reachable.

wsIdleTimeoutnumberOptional

Seconds of inactivity before an idle WebSocket is closed. Tune against any proxy idle timeouts in front of the service.

Default120

Acknowledgement (delivery guarantees)#

For messages that matter, the ACK system holds a pending message until the client confirms receipt, redelivering when the user reconnects and giving up after a bounded number of attempts.

ackobjectOptional

At-least-once delivery configuration.

enabledbooleanOptional

Turn on acknowledgement tracking (on by default when pubsub is enabled). With it off, event frames carry no messageId and nothing is persisted or redelivered.

Defaulttrue
ttlSecondsnumberOptional

How long a pending (unacked) message is retained before expiry.

Default300
maxRetriesnumberOptional

Retry attempts before the message is dropped.

Default3
retryIntervalMsnumberOptional

Accepted and stored in config but never consumed — it has no runtime effect. There is no interval-based retry loop; unacked messages are redelivered when the user reconnects, not on a timer.

Default5000

Presence & cleanup#

Presence broadcasts online/offline transitions to interested clients; the cleanup loop removes stale connection records so memory and counts stay accurate.

presence{ enabled?: boolean; debounceMs?: number }Optional

Broadcast user presence (enabled default true). debounceMs (default 5000) coalesces rapid connect/disconnect flaps into a single update.

cleanupIntervalMsnumberOptional

Interval (default every 60s) for the periodic client-count log. Stale clients are actually pruned lazily — a failed send or a WebSocket close unregisters them; a value <= 0 disables just the log timer.

Default60000

Under the hood — two transports#

Pub/sub spans two worlds: the WebSocket server your browser clients connect to, and a Dapr publish client for service-to-service messaging. The same logical topics flow through both.

WebSocket serverbrowser clientsOptional

Clients connect at wsPath and subscribe to topics; the server applies the ack and presence behaviour from the config above (acknowledged delivery, presence broadcasts, heartbeats). This is what the usePubSub hook on the Realtime page talks to.

DaprPubSubClientservice-to-serviceOptional

For cross-service messaging the Dapr transport exposes publish(topic, data) and publishBulk(topic, messages[]) against a named pubsub component, with per-message metadata and content type — so one service can emit an event that another consumes via its Dapr subscription.

bulk publishBulkPublishResponseOptional

publishBulk sends many messages in one request and returns failedEntries (entryId + error) for any that didn't make it, so a partial failure is observable rather than silent.

Related sections