docs/architecture/overview.md

Architecture

T3 Code runs as a Node.js WebSocket server that wraps codex app-server (JSON-RPC over stdio) and serves a React web app.

1┌─────────────────────────────────┐
2│ Browser (React + Vite) │
3│ wsTransport (state machine) │
4│ Typed push decode at boundary │
5└──────────┬──────────────────────┘
6 │ ws://localhost:3773
7┌──────────▼──────────────────────┐
8│ apps/server (Node.js) │
9│ WebSocket + HTTP static server │
10│ ServerPushBus (ordered pushes) │
11│ ServerReadiness (startup gate) │
12│ OrchestrationEngine │
13│ ProviderService │
14│ CheckpointReactor │
15│ RuntimeReceiptBus │
16└──────────┬──────────────────────┘
17 │ JSON-RPC over stdio
18┌──────────▼──────────────────────┐
19│ codex app-server │
20└─────────────────────────────────┘

Components

  • Browser app: The React app renders session state, owns the client-side WebSocket transport, and treats typed push events as the boundary between server runtime details and UI state.

  • Server: apps/server is the main coordinator. It serves the web app, accepts WebSocket requests, waits for startup readiness before welcoming clients, and sends all outbound pushes through a single ordered push path.

  • Provider runtime: codex app-server does the actual provider/session work. The server talks to it over JSON-RPC on stdio and translates those runtime events into the app's orchestration model.

  • Background workers: Long-running async flows such as runtime ingestion, command reaction, and checkpoint processing run as queue-backed workers. This keeps work ordered, reduces timing races, and gives tests a deterministic way to wait for the system to go idle.

  • Runtime signals: The server emits lightweight typed receipts when important async milestones finish, such as checkpoint capture, diff finalization, or a turn becoming fully quiescent. Tests and orchestration code wait on these signals instead of polling internal state.

  • Server updates: A connected environment advertises whether its server can replace itself. When client and server versions differ, the browser selects an automatic, desktop-managed, or manual update path without changing connection ownership. See Server Update Architecture.

Event Lifecycle

Startup and client connect

mermaid
1sequenceDiagram
2 participant Browser
3 participant Transport as WsTransport
4 participant Server as wsServer
5 participant Layers as serverLayers
6 participant Ready as ServerReadiness
7 participant Push as ServerPushBus
8
9 Browser->>Transport: Load app and open WebSocket
10 Transport->>Server: Connect
11 Server->>Layers: Start runtime services
12 Server->>Ready: Wait for startup barriers
13 Ready-->>Server: Ready
14 Server->>Push: Publish server.welcome
15 Push-->>Transport: Ordered welcome push
16 Transport-->>Browser: Hydrate initial state
  1. The browser boots WsTransport and registers typed listeners in wsNativeApi.
  2. The server accepts the connection in wsServer and brings up the runtime graph defined in serverLayers.
  3. ServerReadiness waits until the key startup barriers are complete.
  4. Once the server is ready, wsServer sends server.welcome from the contracts in ws.ts through ServerPushBus.
  5. The browser receives that ordered push through WsTransport, and wsNativeApi uses it to seed local client state.

User turn flow

mermaid
1sequenceDiagram
2 participant Browser
3 participant Transport as WsTransport
4 participant Server as wsServer
5 participant Provider as ProviderService
6 participant Codex as codex app-server
7 participant Ingest as ProviderRuntimeIngestion
8 participant Engine as OrchestrationEngine
9 participant Push as ServerPushBus
10
11 Browser->>Transport: Send user action
12 Transport->>Server: Typed WebSocket request
13 Server->>Provider: Route request
14 Provider->>Codex: JSON-RPC over stdio
15 Codex-->>Ingest: Provider runtime events
16 Ingest->>Engine: Normalize into orchestration events
17 Engine-->>Server: Domain events
18 Server->>Push: Publish orchestration.domainEvent
19 Push-->>Browser: Typed push
  1. A user action in the browser becomes a typed request through WsTransport and the browser API layer in nativeApi.
  2. wsServer decodes that request using the shared WebSocket contracts in ws.ts and routes it to the right service.
  3. ProviderService starts or resumes a session and talks to codex app-server over JSON-RPC on stdio.
  4. Provider-native events are pulled back into the server by ProviderRuntimeIngestion, which converts them into orchestration events.
  5. OrchestrationEngine persists those events, updates the read model, and exposes them as domain events.
  6. wsServer pushes those updates to the browser through ServerPushBus on channels defined in orchestration.ts.

Async completion flow

mermaid
1sequenceDiagram
2 participant Server as wsServer
3 participant Worker as Queue-backed workers
4 participant Cmd as ProviderCommandReactor
5 participant Checkpoint as CheckpointReactor
6 participant Receipt as RuntimeReceiptBus
7 participant Push as ServerPushBus
8 participant Browser
9
10 Server->>Worker: Enqueue follow-up work
11 Worker->>Cmd: Process provider commands
12 Worker->>Checkpoint: Process checkpoint tasks
13 Checkpoint->>Receipt: Publish completion receipt
14 Cmd-->>Server: Produce orchestration changes
15 Checkpoint-->>Server: Produce orchestration changes
16 Server->>Push: Publish resulting state updates
17 Push-->>Browser: User-visible push
  1. Some work continues after the initial request returns, especially in ProviderRuntimeIngestion, ProviderCommandReactor, and CheckpointReactor.
  2. These flows run as queue-backed workers using DrainableWorker, which helps keep side effects ordered and test synchronization deterministic.
  3. When a milestone completes, the server emits a typed receipt on RuntimeReceiptBus, such as checkpoint completion or turn quiescence.
  4. Tests and orchestration code wait on those receipts instead of polling git state, projections, or timers.
  5. Any user-visible state changes produced by that async work still go back through wsServer and ServerPushBus.