One signal, one query, every viewer

Two-tier real-time: entity-upsert streams content directly to active viewers via SSE, while poke signals keep Replicache in sync for everything else.

POSTGRES source of truth chat · chart entities AI AGENT chat workflow chart workflow INSERT content PG NOTIFY { contentIds } lightweight signal notify SNAPSHOT BACKEND debounce 16ms fetch delta rows fan-out to N SELECT WHERE id IN (...) delta rows Viewer A → React Query Viewer B → React Query Viewer C → React Query entity-upsert via SSE pipe brotli compressed i Replicache poke → pull org · user state (slow path) 1 2 3 4 fast path: write signal 1 delta query N viewers content streams to React Query caches instantly · Replicache syncs org state in the background

1 AI Agent writes to Postgres

The chat or chart workflow (running on the server) streams AI-generated content and writes each chunk to Postgres as ChatMessageContent rows.

After each write, the agent fires pg_notify with the IDs of the rows it just inserted — keeping the signal lightweight while telling downstream exactly what changed.

2 PG NOTIFY signal

PostgreSQL's built-in NOTIFY broadcasts a lightweight signal to all listening server processes. The payload carries only { contentIds } — no actual row data.

This decouples the write path from the read path: the agent doesn't need to know who's listening or how many viewers are connected. PubSub handles the fan-out of the signal, not the data.

3 Snapshot Backend

Receives signals and debounces them in a 16ms window — coalescing rapid-fire token writes into a single database read.

Fetches only the delta: SELECT WHERE id IN (...) using the content IDs from the signal. One query serves all connected viewers on that topic, regardless of how many there are.

This is the key efficiency: N viewers watching the same chat share one DB query, not N queries.

4 Fan-out via SSE

Each browser tab holds one SSE connection that multiplexes all its topic subscriptions (org, user, chat, chart). No per-entity connections needed.

The delta rows are serialized as entity-upsert ops and pushed over the SSE pipe with brotli compression (quality 4). The client applies them directly to React Query caches — bypassing Replicache for instant UI updates.

Org-level state changes go through a separate poke → Replicache pull slow path for consistency.

i SSE connection lifecycle

Each browser tab opens one SSE connection, multiplexing all topics over a single HTTP stream:

  1. On mount, StreamProvider opens GET /api/sse/stream?topics=org:…,user:… — the server validates access to each topic, then holds the response open.
  2. As the user navigates (opens a chat, views a chart), useStreamSubscription adds topics to the set. The client closes and reopens the EventSource with the updated ?topics= query param.
  3. On the server, the TopicManager does PG LISTEN signal:chat:… when the first client subscribes to a topic, and UNLISTEN when the last disconnects.
  4. On error, the client retries with exponential backoff (1s → 2s → 4s … max 30s). After 10 consecutive failures it stops. Any successful message resets the counter.

There's no explicit subscribe/unsubscribe API — topic changes always trigger a clean reconnect with the full topic list.

Event stream

Zoom: the write path

click to expand

steps 1 → 2

AI PROVIDER OpenAI / Anthropic streaming response tokens TEXT ACCUMULATOR "The revenue chart shows..." append(delta) → dirty = true full text, not incremental EVENT BATCHER 100ms batch interval time-based only FLUSH! POSTGRES INSERT ... ON CONFLICT DO UPDATE SET content = EXCLUDED.content RETURNING id → contentIds[] PG NOTIFY signal:chat:{chatId} { contentIds, messageId } payload < 8KB → Snapshot Backend no size limit — time-based flush full text upsert, not deltas signal carries IDs, not content idempotent via ON CONFLICT ~100ms window upsert + RETURNING

Zoom: the snapshot backend

click to expand

step 3 → 4

PG NOTIFY signal:chat:{id} { contentIds } DEBOUNCE 16ms window pendingPayloads: Map<topic, []> contentIds merged via Set signals coalesced → 1 fetch resets on each signal POSTGRES SELECT * FROM "ChatMessageContent" WHERE id IN (...contentIds) fetchChatSnapshot() delta rows + parent ChatMessage rows (deduced from content.chatMessageId) SERIALIZE EntityUpsertOp[] { entity: "chatMessage", data: { id, content, ... } } { entity: "chatMessageContent" } JSON.stringify Viewer A SSE pipe · brotli q4 → React Query cache Viewer B SSE pipe · brotli q4 → React Query cache Viewer C SSE pipe · brotli q4 → React Query cache _notifyListeners() same payload → N send() dots shrink = brotli _listeners topology Map<topic, Map<clientId, sendFn>> chat:abc → { "sess_1": send, "sess_2": send } chat:abc → { "sess_3": send } first listener → pgPubSub.listen() last unlistener → pgPubSub.unlisten() N viewers share 1 DB query 16ms coalesces rapid tokens dynamic LISTEN/UNLISTEN TopicManager routing chat:* → SnapshotBackend chart:* → SnapshotBackend org:* → PokeBackend user:* → PokeBackend

Zoom: the viewer

click to expand

inside one browser tab

EventSource /api/sse/stream unified per-tab pipe brotli decompressed <StreamProvider> useEntityUpsert Subscription parse entity-upsert ops for (op of event.ops) { setQueryData(key, upsert) } queryClient cache ['chats', orgId] ChatEntity[] ['chatMessages', chatId] ChatMessageEntity[] ['chatMessageContents', id] ChatMessageContentEntity[] upsertEntity(old, new) match by id → shallow merge no id match → append to array TanStack DB collections chatMessagesCollection chatMessageContentsCollection observes cache keys collectionRegistry (singleton per scope) useChatMessages useLiveQuery(q => q.from({ messages, contents }) ) joins messages + contents in memory notify Component tree DashboardChatContent UserMessage (unchanged) AssistantMessage (old) AssistantMessage ↑ re-renders ↑ only changed message re-renders Structural sharing React Query compares by reference: unchanged entities → same ref ✓ changed entities → new ref → render minimizes re-renders automatically 1 SSE pipe multiplexes all topics setQueryData bypasses fetch cycle TanStack DB → collection-level reactivity useLiveQuery subscribes to collections only changed message re-renders SSE event setQueryData collection notifies 1 component re-renders