Web
Lets a Chat SDK bot serve a browser chat UI alongside Slack, Teams, Discord — the same handler fires for every platform. Speaks the AI SDK UI message stream protocol so useChat works out of the box with React, Vue, and Svelte.
Install
pnpm add @chat-adapter/web aiThen install the framework package that matches your UI:
| Framework | Package | Import from |
|---|---|---|
| React / Next.js | @ai-sdk/react | @chat-adapter/web/react |
| Vue / Nuxt | @ai-sdk/vue | @chat-adapter/web/vue |
| Svelte / SvelteKit | @ai-sdk/svelte | @chat-adapter/web/svelte |
Quick start
import { Chat } from "chat";
import { createWebAdapter } from "@chat-adapter/web";
import { createMemoryState } from "@chat-adapter/state-memory";
export const bot = new Chat({
userName: "mybot",
adapters: {
web: createWebAdapter({
userName: "mybot",
getUser: (req) => ({ id: getUserIdFromCookie(req) }),
}),
},
state: createMemoryState(),
});
bot.onDirectMessage(async (thread, message) => {
await thread.post(`You said: ${message.text}`);
});import { after } from "next/server";
import { bot } from "@/lib/bot";
export async function POST(request: Request): Promise<Response> {
return bot.webhooks.web(request, {
waitUntil: (task) => after(() => task),
});
}"use client";
import { useChat } from "@chat-adapter/web/react";
export default function ChatPage() {
const { messages, sendMessage, status, stop } = useChat();
// Render with `ai-elements` (<Conversation>, <Message>, <PromptInput>)
// or your own components — `messages`, `sendMessage`, `status` are the
// standard AI SDK UI API.
}Configuration
Prop
Type
Authentication
getUser is the security boundary for the Web adapter. Unlike Slack or Teams where the platform signs every webhook, web requests come straight from a browser — you must identify the caller yourself.
// NextAuth
createWebAdapter({
userName: "mybot",
getUser: async (req) => {
const session = await getServerSession(authOptions);
if (!session?.user) return null;
return { id: session.user.id, name: session.user.name };
},
});
// Clerk
createWebAdapter({
userName: "mybot",
getUser: async () => {
const { userId, sessionClaims } = await auth();
if (!userId) return null;
return { id: userId, name: sessionClaims?.name as string | undefined };
},
});The resolved user.id is embedded in the Chat SDK thread id. Ids containing : are rejected with HTTP 400 — normalize them inside getUser (e.g. base64-encode them) if your auth provider emits ids like provider:sub.
Advanced
Threading
By default, each useChat conversation maps to one Chat SDK thread:
web:{user.id}:{conversationId}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.
Override with threadIdFor if you want a single thread per user:
createWebAdapter({
userName: "mybot",
getUser,
threadIdFor: ({ user }) => `web:${user.id}:default`,
});The encode/decode helpers are available on the adapter:
adapter.encodeThreadId({ userId: "u1", conversationId: "abc" });
// → "web:u1:abc"
adapter.decodeThreadId("web:u1:abc");
// → { userId: "u1", conversationId: "abc" }Streaming
thread.post accepts an AsyncIterable<string | StreamChunk> and pumps deltas straight onto the SSE response — no edit loop, no rate limiting. Plays nicely with streamText from the AI SDK:
import { streamText } from "ai";
bot.onDirectMessage(async (thread, message) => {
const result = streamText({ model, prompt: message.text });
await thread.post(result.textStream);
});The adapter honors request.signal, so calling stop() from useChat short-circuits the iterator on the server.
Message persistence
persistMessageHistory defaults to true. Web has no platform-side history API, so the only way for handlers to see prior turns via thread.messages is through the configured state adapter's cache. Set it to false only if your handler re-derives history from the request body's messages[].
Framework integrations
The Web adapter speaks the AI SDK UI message stream protocol, so React, Vue, and Svelte AI SDK clients work against the same server endpoint. The framework subpaths below expose useChat helpers preconfigured for that endpoint.
React — @chat-adapter/web/react ships a thin convenience wrapper preconfigured with DefaultChatTransport. It accepts a few extra options on top of the standard @ai-sdk/react API:
import { useChat } from "@chat-adapter/web/react";
const { messages, sendMessage, status, stop, regenerate } = useChat({
api: "/api/chat",
threadId: "support-1",
});| Option | Description |
|---|---|
api | API endpoint for the Web adapter route. Defaults to /api/chat. |
threadId | Chat SDK thread id — surfaces in the request body's id. Strongly recommended. |
experimental_throttle | Throttle wait in ms for chat messages and data updates. |
resume | Whether to resume an ongoing chat generation stream. |
| ...rest | All other options pass through to @ai-sdk/react's useChat. |
For advanced configuration, use @ai-sdk/react's useChat directly — there's nothing magical in the wrapper.
Vue / Nuxt — @chat-adapter/web/vue exports a useChat factory that returns a Chat instance (from @ai-sdk/vue) whose messages, status, and error properties are Vue-reactive. Access them directly in your template — do not destructure, as that breaks Vue's reactivity tracking:
<script setup lang="ts">
import { useChat } from "@chat-adapter/web/vue";
const chat = useChat({ api: "/api/chat", threadId: "support-1" });
</script>
<template>
<div v-for="msg in chat.messages" :key="msg.id">
<template v-for="part in msg.parts">
<p v-if="part.type === 'text'">{{ part.text }}</p>
</template>
</div>
</template>Svelte / SvelteKit — @chat-adapter/web/svelte exports the same factory, returning a Chat instance (from @ai-sdk/svelte) with Svelte 5 $state-backed reactive properties:
<script lang="ts">
import { useChat } from "@chat-adapter/web/svelte";
const chat = useChat({ api: "/api/chat", threadId: "support-1" });
</script>
{#each chat.messages as msg (msg.id)}
{#each msg.parts as part}
{#if part.type === "text"}<p>{part.text}</p>{/if}
{/each}
{/each}Unlike the React wrapper which wraps @ai-sdk/react's useChat hook and returns destructurable helpers, the Vue and Svelte wrappers return a Chat class instance — the reactive state lives on the object itself. The api and threadId options are identical across all three, and the server-side setup never changes.
Feature support
Messaging
| Feature | Supported |
|---|---|
| Post message | |
| Edit message | |
| Delete message | |
| File uploads | |
| Streaming | Native (SSE) |
| Scheduled messages |
Rich content
| Feature | Supported |
|---|---|
| Card format | Markdown only (v1) |
| Buttons | |
| Link buttons | |
| Select menus | |
| Tables | GFM |
| Fields | |
| Images in cards | |
| Modals |
Conversations
| Feature | Supported |
|---|---|
| Slash commands | |
| Mentions | |
| Add reactions | |
| Remove reactions | |
| Typing indicator | |
| DMs | |
| Ephemeral messages | |
| User lookup | |
| Parent subject | |
| Native client | |
| Custom API endpoint |
Message history
| Feature | Supported |
|---|---|
| Fetch messages | State cache |
| Fetch single message | |
| Fetch thread info | Synthesized |
| Fetch channel messages | State cache |
| List threads | |
| Fetch channel info | |
| Post channel message |