Today Platform Web — Dev Docs
Architecture

Chat runtime

A high-level overview of the chat-v2 runtime — Postbox-style store, SSE-driven live source, paginated history, holes — with pointers to the full local docs under `apps/web/docs/chat/`.

The chat feature in apps/web (route /chat-v2, plus the embedded /agent shell) runs on a custom runtime that the team built rather than adapting an off-the-shelf chat library. This page is the architectural overview; the implementation details live in apps/web/docs/chat/, which is the canonical source for anyone touching the code.

Three-layer split

apps/web/src/features/chat-v2/       Business glue: composes store + protocol + UI
apps/web/src/lib/chat/                Data layer: zustand store, dexie persistence, SSE source
design-system/tdx-ui/src/chat/        Presentation: MessageList, MessageBubble, Composer, MediaViewer

The boundary rules:

  • tdx-ui/src/chat is purely presentational. It receives data, emits intents through callbacks. It must not import from apps/web, the lib store, or any network code.
  • apps/web/src/lib/chat knows nothing about React rendering. It exports plain functions and a zustand store. Components subscribe via thin selector hooks in features/chat-v2.
  • apps/web/src/features/chat-v2 is the only place that wires the store, the agent client, the SSE source, and the tdx-ui primitives together.

This split is enforced by code review; there are no ESLint rules pinning it today.

Data model — Postbox port

The data layer is a TypeScript port of the iOS TDChat Postbox layer. Where Swift uses an actor, TS uses a single zustand store with a write queue (sequential Promise chain) to preserve ordering. Where Swift uses GRDB, TS uses Dexie over IndexedDB.

Core concepts (full definitions in apps/web/docs/chat/architecture.md):

  • MessageID — peer-scoped, namespace-tagged identity. local messages live alongside cloud ones until the server ack arrives, then get rewritten to the real id.
  • MessageIndex — sort key (timestamp, namespace, id). local sorts before cloud at the same timestamp so optimistic messages render in place until ack.
  • MessageWindow — a sliding window of entries around an anchor. Entries are either message or hole (an unloaded range).
  • HoleSet — sorted set of half-open ranges, merged on overlap. nearest(index, direction) finds the next hole to fill; scrolling into one triggers MessageStore.fillHole(range).

Live + history dual sources

SourceRole
apps/web/src/lib/chat/runtime/live-source.tsSSE consumer — thread.token, message.new, thread.complete, etc. Applies updates into the store.
apps/web/src/lib/chat/runtime/remote-loader.tsHTTP history loader — fills holes when the user scrolls past loaded messages.

Both sources feed the same MessageStore.apply(updates) API. Dedup is handled by noteOutgoingDispatched(fingerprint) + hasKnownServerId() so the live source doesn't write the same message twice.

Per-session store lifecycle

The store is created once per session in features/chat-v2/providers/chat-runtime-provider.tsx and passed through React context. There's no global singleton — the provider is mounted under apps/web/src/app/(agent)/(opal)/layout.tsx so the chat session survives navigation between sub-routes.

Persistence

Dexie schema, one IndexedDB database per userId:

db.version(1).stores({
  messages: '[peerId+namespace+id], peerId, [peerId+timestamp]',
  message_holes: '++autoId, peerId',
  chat_state: 'peerId', // last cursor, lastReadMessageId
  known_ids: '[peerId+fingerprint]', // dedup set
})

Per-user isolation lets a multi-account session swap stores without cross-contaminating message data. The keyed dedup index is what makes the live + history dual-source story safe.

Token streaming

docs/changes/2026-03-22-token-streaming-frontend.md documents the implementation that landed for admin chat. The web chat follows the same pattern: the backend emits thread.token SSE events per token; the frontend appends them to a streaming buffer in the store; once thread.complete fires, the streaming message becomes a normal message with its final fingerprint.

Where to read further

The chat runtime is the most complex single subsystem in apps/web. For anything beyond the overview above, read:

These docs live next to the code they describe by design — they change together. This site links out rather than mirroring them to avoid drift.

  • API codegenchat-runtime consumes the generated agent client (apps/web/src/lib/chat/protocol/agent-client.ts)
  • MSW mocks — chat handlers in apps/web/src/lib/msw/handlers/agent.ts mock the SSE stream for Storybook + unit tests
  • Workspace → Apps → @todayai-labs/web — top-level package overview

On this page