@chat-adapter/web
Web adapter for Chat SDK. Lets a chat-sdk bot serve a browser chat UI alongside Slack, Teams, Discord, etc. — the same bot.onDirectMessage(...) handler fires for every platform.
The adapter speaks the AI SDK UI message stream protocol, so @ai-sdk/react's useChat and the ai-elements component library work out of the box.
Installation
Quick start
Server
Client
Authentication
getUser is the security boundary for the Web adapter. Unlike Slack/Teams where the platform signs every webhook, web requests come straight from a browser — you must identify the caller yourself. Returning null causes the adapter to respond with HTTP 401 and no handler runs.
Plug in whatever your app already uses:
If getUser throws, the adapter returns 401 and logs the error. Don't include sensitive data in the error message — it's not surfaced to the client, but it is logged.
The resolved
user.idis embedded in the chat-sdk thread id (see Threading below). User ids containing:are rejected with HTTP 400 because they would corrupt the round-trip throughdecodeThreadId. If your auth provider emits ids with colons (e.g.provider:subclaims), normalize them insidegetUser— for example by base64-encoding.
Threading
By default, each useChat conversation maps to one chat-sdk thread:
conversationId is the id field useChat sends in its request body. If your client supplies one (useChat({ id: "support-chat" })), it's reused across reloads; otherwise a fresh id is generated per request.
channel.messages and thread.messages are equivalent on web — the channel id is the thread id. This avoids cross-conversation bleed when persistMessageHistory is enabled and the same user has multiple useChat conversations open.
To override (for example, one thread per user regardless of conversation):
The encode/decode helpers are exposed on the adapter:
Streaming
thread.post accepts an AsyncIterable<string | StreamChunk> and pumps deltas straight onto the SSE response body — no edit loop, no rate limiting. Plays nicely with the AI SDK's streamText:
The adapter honors request.signal, so calling stop() from useChat short-circuits the iterator on the server. task_update and plan_update StreamChunks have no native v1 representation in the UI message stream and are dropped silently.
Message persistence
persistMessageHistory defaults to true. Web has no platform-side history API, so the only way for chat-sdk handlers to see prior turns via thread.messages / channel.messages is through the configured state adapter's message history cache. Set it to false only if your handler re-derives history from the request body's messages[] itself:
The AI SDK client retains the conversation in its UI state and resends it on every request, so opting out is a valid choice for stateless handlers — but anything that calls await thread.messages won't see prior turns.
React hook
@chat-adapter/web/react exports a thin wrapper around @ai-sdk/react's useChat preconfigured with DefaultChatTransport:
For advanced configuration (custom transport, response interceptors, etc.) use @ai-sdk/react's useChat directly — there's nothing magical in the wrapper.
Configuration
Features
Messaging
Rich content
Conversations
Message history
v1 scope
In: text + markdown, native streaming, DM-style routing, persisted message history, abort propagation via request.signal.
Out (deferred to v2): cards/JSX rendering, reactions, modals, file uploads, edit/delete, multi-tab proactive push.
Troubleshooting
Every request returns 401
getUseris returningnullor throwing. Add a log inside it to confirm the request actually carries the session you expect.- Cookies aren't being forwarded — check that
useChatis mounted on the same origin as/api/chat(or that your transport passes credentials).
Every request returns 400 "Invalid user id"
- The id returned by
getUsercontains a:character, which would corrupt the thread-id round-trip. Normalize the id insidegetUser(for example,id.replace(/:/g, "_")or base64-encode it).
useChat recreates state on every render
- Don't pass
id: undefinedtouseChat. The wrapper guards against this internally — but if you're calling@ai-sdk/react'suseChatdirectly, omitidrather than passingundefined.
thread.messages is empty
persistMessageHistoryisfalseand there is no platform-side history to fall back on. Either set it totrue(the default) or read history from the request body'smessages[]directly inside your handler.
License
MIT