POST /api/chatruns the model and callschat.makeResumable(...), which appends every chunk to a per-turn S2 stream and serves the client by reading that same stream back as SSE. Each SSEidis the S2 sequence number.- On reconnect,
useChat({ resume: true })callsGET /api/chat/:id/stream, which replays the in-flight turn from S2 to completion. - Completed messages are stored on a transcript stream and reloaded on open.
Streams are created on first append or read (the basin is configured with
--create-stream-on-append --create-stream-on-read). There are two per chat:
a transcript stream and a single-use stream per assistant turn.
Ask for a sales report and the agent calls run_analytics_query, which streams
result rows to an S2 stream. The query fails partway through twice before it
succeeds. DBOS retries it, and each retry replaces the
failed attempt's rows on the stream, so the chat shows one clean result. The
retries are part of the resumable chat stream, so they can work with a refresh too.
Code: server/src/durable-query.ts, server/src/fenced-stream.ts.
Needs Postgres for DBOS: run npx dbos postgres start or set
DBOS_SYSTEM_DATABASE_URL.
Create a basin:
export S2_ACCESS_TOKEN="..."
s2 create-basin my-basin --create-stream-on-append --create-stream-on-readBackend (needs Postgres for the durable tool):
npx dbos postgres start # or point DBOS_SYSTEM_DATABASE_URL at any Postgres
cd server
cp .env.example .env # set S2_ACCESS_TOKEN, S2_BASIN, OPENAI_API_KEY, DBOS_SYSTEM_DATABASE_URL
npm install
npm run dev # http://localhost:8080App:
cd app
cp .env.example .env # set EXPO_PUBLIC_API_URL
npm install
npm run web # or: npm run ios | npm run android- Switch model provider with
CHAT_PROVIDER=openai|anthropicinserver/.env. - React Native's built-in
fetchbuffers responses, which breaks streaming SSE. The app usesexpo/fetch, which streams, and passes it to the AI SDK transport. Seeapp/src/app/index.tsx. - Deploy the backend to Cloud Run with the included
server/Dockerfile.
To run without a cloud token, use s2 lite (in-memory) and point the backend at it:
s2 lite --port 9000
# server/.env:
# S2_ACCESS_TOKEN=ignored
# S2_ACCOUNT_ENDPOINT=http://localhost:9000
# S2_BASIN_ENDPOINT=http://localhost:9000