This example runs the Server-side history pre-load
recipe end-to-end in a Next.js 15 App Router app. A server component fetches
the conversation transcript inside the request (loadConversation(id)), passes
the result into initialMessages, and the client component scopes
persistenceKey to the conversation id so follow-up turns are cached in the
browser.
It is a strict superset of examples/with-next: the OpenAI SSE
route handler is identical, and this app adds the server-fetch wiring on top.
| Route | What it shows |
|---|---|
/ |
Server-rendered list of demo conversation ids and a link to /c/new. |
/c/[id] |
Server component calls loadConversation(id) and seeds <Chorus initialMessages={...}>; the client wrapper sets persistenceKey={chorus:c:${id}} so follow-up turns are cached per conversation. |
/c/new |
Server component that redirects to /c/<server-generated-uuid>. Mirrors the loader-redirect alternative documented alongside the useId / useEffect patterns in the guide — the URL is the source of truth for the conversation id, so each new chat starts with an empty stored payload. |
/api/chat |
The same OpenAI SSE route handler as examples/with-next: consumes Chorus's { prompt, history } payload, maps history with toOpenAIChatCompletionsBody, and re-emits the OpenAI stream as SSE using react-chorus/server helpers. |
<Chorus> resolves the visible transcript in this order on every mount when
persistenceKey is set without value:
- A stored payload for this key wins.
initialMessagesis silently dropped from the visible transcript whenpersistenceStorage.getItem(key)returns a non-empty value. - No stored payload →
initialMessagesis rendered AND written to storage. First visit seeds the cache. - Async adapters block the composer until
getItem()resolves.
The asymmetry is intentional but easy to get wrong: once a browser has any
stored payload under the key, a fresh server fetch is only a fallback on
mount. If you want server writes from another device to win on reload,
either drop persistenceKey entirely (and use controlled value + onChange
with a write API), or read the stored payload yourself before mount and call
localStorage.removeItem(key) when the server transcript is newer. See the
Choosing what to trust table in
the recipe for the full matrix.
- Node.js 20.19+ or 22.12+ — the floor declared in this example's
engines.node(same as every other example in this repo). - An
OPENAI_API_KEY. Without it the route handler throwsMissing OPENAI_API_KEYon the first send and the UI renders a connection-style error.
npm install
npm run buildcd examples/with-next-resume
npm installSet your API key with the command for your shell, then start Next.js:
# macOS/Linux/POSIX shells
OPENAI_API_KEY=sk-... npm run dev
# Windows PowerShell
$env:OPENAI_API_KEY="sk-..."; npm run dev
# Windows cmd.exe
set OPENAI_API_KEY=sk-... && npm run devNext prints the local URL (usually http://localhost:3000). Open it, follow a
saved conversation to see the server transcript pre-load, then click
+ Start a fresh conversation to land on a unique /c/<uuid> with empty
state.
lib/conversations.ts— the stubloadConversation(id)with hard-coded fixtures. In a real app this is where you authorize against the current session and query your database.app/c/[id]/page.tsx— server component that awaitsloadConversationand renders the client wrapper.app/c/[id]/ChatClient.tsx— the client component wiringinitialMessages+persistenceKeyonto<Chorus>.app/c/new/page.tsx— fresh-conversation route that redirects to a server-generated uuid.app/api/chat/route.ts— the OpenAI SSE proxy, unchanged fromexamples/with-next.
Cannot find module 'react-chorus'— you skipped the repo-root build. Runnpm install && npm run buildat the repository root, then re-runnpm installinexamples/with-next-resume.- A fresh conversation shows an old transcript — your browser has a
stored payload under the same
persistenceKey. Either visit/c/newto get a new uuid, or open DevTools → Application → Local Storage and remove the matchingchorus:c:<id>entry. The precedence rule above is doing exactly what it is documented to do. Missing OPENAI_API_KEY—OPENAI_API_KEYwas not set in the shell that startednpm run dev. Stop the dev server, export the key, and start it again.