Web

Serve a browser chat UI from the same Chat SDK bot and handlers you use for Slack, Teams, and Discord. Speaks the AI SDK UI message stream protocol, so useChat works with React, Vue, and Svelte.

Create a new bot

Scaffold a Next.js bot with the Web adapter using create-chat-sdk:

pnpm create chat-sdk@latest my-bot --adapter web memory -y

The generated app uses the Memory state adapter for local development. To add the adapter to an existing app, start at Install.

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`, and `status` are the
  // standard AI SDK UI API.
}

Authentication

The Web adapter has no platform credentials. Browser requests arrive without a platform signature, so your getUser function is the security boundary: it identifies the caller from your own app's session, and returning null rejects the request with HTTP 401.

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, so normalize them inside getUser (for example, by base64-encoding them) if your auth provider emits IDs like provider:sub.

Configuration

Prop

Type

Streaming

thread.post accepts an AsyncIterable<string | StreamChunk> and writes deltas directly to the SSE response, without the post-and-edit loop or rate limiting other adapters need. You can pass streamText output 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.

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 expose useChat helpers preconfigured for that endpoint. The api and threadId options are the same in all three, and the server-side setup doesn't change.

React

@chat-adapter/web/react is a thin wrapper around @ai-sdk/react's useChat, preconfigured with DefaultChatTransport. It returns destructurable helpers and accepts a few extra options on top of the standard 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, sent as the request body's id. 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 anything the wrapper doesn't cover, use @ai-sdk/react's useChat directly.

Vue / Nuxt

@chat-adapter/web/vue exports a useChat factory that returns a Chat instance from @ai-sdk/vue. Its messages, status, and error properties are Vue-reactive. Access them directly in your template; destructuring 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. As with Vue, the reactive state lives on the object itself.

<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}

Message persistence

persistMessageHistory defaults to true. Web has no platform-side history API, so handlers can see prior turns through thread.messages only if the configured state adapter has cached them.

The request body's messages[] array is not an alternative history source. A browser controls it entirely, so the adapter consumes only the latest user message and strips tool parts from it. A client therefore cannot inject forged tool-call or approval state or rewrite earlier turns. Text, file, and custom data-* parts pass through to message.raw unchanged. Setting persistMessageHistory: false leaves handlers with only the current message.

Thread IDs

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 the adapter generates a fresh ID per request.

Override threadIdFor to use a single thread per user:

createWebAdapter({
  userName: "mybot",
  getUser,
  threadIdFor: ({ user }) => `web:${user.id}:default`,
});

The adapter exposes encode and decode helpers:

adapter.encodeThreadId({ userId: "u1", conversationId: "abc" });
// → "web:u1:abc"
adapter.decodeThreadId("web:u1:abc");
// → { userId: "u1", conversationId: "abc" }

Feature support

Messaging

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

Rich content

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

Conversations

FeatureSupported
Slash commands
Mentions
Add reactions
Remove reactions
Typing indicator
Mark as read
Message edit events
Message delete events
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

More adapters