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 ai

Then install the framework package that matches your UI:

FrameworkPackageImport 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

lib/bot.ts
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}`);
});
app/api/chat/route.ts
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),
  });
}
app/chat/page.tsx
"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.

lib/bot.ts
// 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",
});
OptionDescription
apiAPI endpoint for the Web adapter route. Defaults to /api/chat.
threadIdChat SDK thread id — surfaces in the request body's id. Strongly recommended.
experimental_throttleThrottle wait in ms for chat messages and data updates.
resumeWhether to resume an ongoing chat generation stream.
...restAll 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

FeatureSupported
Post message
Edit message
Delete message
File uploads
StreamingNative (SSE)
Scheduled messages

Rich content

FeatureSupported
Card formatMarkdown only (v1)
Buttons
Link buttons
Select menus
TablesGFM
Fields
Images in cards
Modals

Conversations

FeatureSupported
Slash commands
Mentions
Add reactions
Remove reactions
Typing indicator
DMs
Ephemeral messages
User lookup
Parent subject
Native client
Custom API endpoint

Message history

FeatureSupported
Fetch messagesState cache
Fetch single message
Fetch thread infoSynthesized
Fetch channel messagesState cache
List threads
Fetch channel info
Post channel message

On this page