Today Platform Web — Dev Docs
Architecture

Chat runtime

Cross-package boundary overview for the chat-v2 runtime in `apps/web`. Implementation details live next to the code 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 cross-package boundary overview; the implementation details — data model, persistence schema, scroll anchoring, in-flight reply handling — 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/opal-ui/src/chat/        Presentation: MessageList, MessageBubble, Composer, MediaViewer

The boundary rules:

  • opal-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 opal-ui primitives together.

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

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 inside the store so the live source doesn't write the same message twice.

Where to read further

The data layer is a TypeScript port of the iOS TDChat Postbox layer (zustand actor-equivalent with a sequential write queue; Dexie over IndexedDB instead of GRDB). All the load-bearing concepts — MessageID peer-scoped identity, MessageIndex sort keys, MessageWindow sliding windows, HoleSet range merging, per-session store lifecycle, persistence schema, token-streaming buffer — live in:

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