# Get started with useChat Build a chat app with the Vercel AI SDK's useChat hook, where each response streams over an Ably channel, so a tab that reloads during a response receives the rest of it. ## What you build A Next.js chat app where: - The model's response streams into `useChat`'s messages over an [Ably channel](https://ably.com/docs/channels.md). - Reloading the page during a response shows the stored conversation, then the response so far, then the rest of it live. - A tab you open during a response shows the response so far, then the rest of it live. - The Stop button stops the agent, and ends the response in the tab. The conversation lives in your database. AI Transport's [chat transport](https://ably.com/docs/ai-transport/api/javascript/vercel/chat-transport.md) takes the place of `useChat`'s default HTTP transport. ## Prerequisites - Node.js 22 or later. - An [Ably account](https://ably.com/sign-up) with an API key. - An API key for Anthropic, or for any other model provider the Vercel AI SDK supports. ## Install dependencies Install the AI Transport SDK, the [Ably Pub/Sub](https://ably.com/docs/pub-sub.md) SDKs, the Vercel AI SDK with its React hooks, and Next.js, then add the scripts that run the app: ### Shell ``` npm install @ably/ai-transport @ably/pubsub-server @ably/pubsub-device ai@^7 @ai-sdk/react @ai-sdk/anthropic next react react-dom npm pkg set scripts.dev="next dev" scripts.build="next build" ``` The agent's HTTP endpoint creates its Ably client with `@ably/pubsub-server`, and the browser creates its client with `@ably/pubsub-device`. AI Transport does not depend on Next.js. This guide uses it as an example. ## Add your keys Create `.env.local` with your Ably API key and your model provider's key, which the server-side code reads: ### Shell ``` ABLY_API_KEY=your-ably-api-key ANTHROPIC_API_KEY=your-anthropic-api-key ``` ## Set up authentication Create `app/api/auth/token/route.js`, an [auth endpoint](https://ably.com/docs/ai-transport/setup/authentication.md#server) that returns an Ably token to the browser. The chat transport needs `subscribe` and `history` to read the response from the channel, and the Stop button needs `publish` to publish its cancel message: ### Javascript ``` // Server code. Only the server holds the API key. import { createHttpClient } from '@ably/pubsub-server'; // Replace with your app's auth: validate the request (session cookie, bearer // token) and return the authenticated user's id. async function authenticateUser(req) { return 'user-abc'; } const ably = createHttpClient({ key: process.env.ABLY_API_KEY }); export async function GET(req) { const userId = await authenticateUser(req); const tokenRequest = await ably.auth.createTokenRequest({ clientId: userId, capability: { 'conversations:*': ['subscribe', 'history', 'publish'] }, }); return Response.json(tokenRequest); } ``` The browser's client fetches its token with `authUrl: '/api/auth/token'`. > **Important** > > In production, the agent's HTTP endpoint should also authenticate each [POST](https://ably.com/docs/ai-transport/setup/authentication.md#agent-post), and check that the user may use the conversation in the request. Narrow the token's capability to the user's conversations too, because `publish` on `conversations:*` lets a client publish to every conversation's channel. ## Configure the channel rule AI Transport streams a response's text by appending each delta to a channel message. That needs the **Message annotations, updates, deletes, and appends** rule (`mutableMessages: true` in the Control API) on the channels your conversations use. The rule also persists the channel's messages, so a tab that reloads can replay a response. This guide uses the channel `conversations:demo`, so enable the rule for `conversations:*`, once per Ably app, through the [dashboard, Control API, or CLI](https://ably.com/docs/ai-transport/setup/channel-rules.md). > **Important** > > Without the rule, the first append fails with error `93002` and the response does not stream. The channel name you use must match the rule's expression. ## Store the conversation Create `app/store.js`. It stands in for your database, and holds each conversation's messages and whether a response is in progress: ### Javascript ``` // Agent code. Replace these functions with queries to your database. const conversations = new Map(); const running = new Set(); export function loadMessages(chatId) { return conversations.get(chatId) ?? []; } export function saveMessages(chatId, messages) { conversations.set(chatId, messages); } // Returns false if the conversation already has a response in progress. export function startTurn(chatId) { if (running.has(chatId)) return false; running.add(chatId); return true; } export function endTurn(chatId) { running.delete(chatId); } ``` ## Create the agent's HTTP endpoint Create `app/api/chat/route.js`. The `POST` handler saves the conversation, and runs the agent with Next.js [`after()`](https://nextjs.org/docs/app/api-reference/functions/after), so the agent keeps running if the user reloads the page. The handler rejects a new message with a 409 while the conversation's previous response is still in progress, because the [chat transport](https://ably.com/docs/ai-transport/api/javascript/vercel/chat-transport.md#one-turn) cannot differentiate two responses on one channel. The agent publishes the user's message to the channel for other clients, pipes the model's output to the channel with [`vercelAdaptor`](https://ably.com/docs/ai-transport/api/javascript/vercel.md#vercel-adaptor) and stops when a `cancel` message arrives on the channel: ### Javascript ``` // Agent code. This runs on your own infrastructure, so it holds the API key. import { createRealtimeClient } from '@ably/pubsub-server'; import { after } from 'next/server'; import { anthropic } from '@ai-sdk/anthropic'; import { convertToModelMessages, streamText, toUIMessageStream } from 'ai'; import { vercelAdaptor } from '@ably/ai-transport/vercel'; import { endTurn, loadMessages, saveMessages, startTurn } from '../../store'; export async function POST(req) { // chatId is useChat's id, which is the conversation's channel name. const { chatId, trigger, messages } = await req.json(); if (!startTurn(chatId)) return new Response('A response is already streaming', { status: 409 }); saveMessages(chatId, messages); // Call endTurn however the agent ends, even if the agent cannot start. after(() => respond(chatId, trigger, messages).finally(() => endTurn(chatId))); return new Response(null, { status: 202 }); } async function respond(chatId, trigger, messages) { // A client for this response only. Closing the client when the response ends also removes the cancel subscription. const ably = createRealtimeClient({ key: process.env.ABLY_API_KEY }); const channel = ably.channels.get(chatId); const agent = vercelAdaptor(channel); const controller = new AbortController(); try { // Subscribing attaches the channel. Wait until the channel is attached before publishing anything, // so a cancel message sent after the response starts reaches this agent. await channel.subscribe('cancel', () => controller.abort()); // Publish a new prompt for other clients. A regenerated response reuses a prompt already on // the channel, and a tool result arrives as 'submit-message' with the assistant's message last. const prompt = messages.at(-1); if (trigger === 'submit-message' && prompt.role === 'user') { await agent.send({ type: 'user-message', message: prompt }); } const result = streamText({ model: anthropic('claude-sonnet-4-20250514'), messages: await convertToModelMessages(messages), // Abort the model call only, so a stopped response still ends with an abort chunk. abortSignal: controller.signal, }); await agent.pipe( toUIMessageStream({ stream: result.stream, originalMessages: messages, // Give each response a new id, which useChat and the store share. generateMessageId: () => crypto.randomUUID(), onEnd: ({ messages: conversation }) => saveMessages(chatId, conversation), }), ); } catch (error) { // useChat waits until a chunk ends the response, so publish error. console.error(error); await agent.send({ type: 'error', errorText: 'The agent failed to respond.' }); } finally { ably.close(); } } export function GET(req) { const chatId = new URL(req.url).searchParams.get('chat'); return Response.json({ messages: loadMessages(chatId) }); } ``` > **Note** > > `after()` runs the agent after the HTTP response is sent, so the agent's errors appear in the dev server's terminal. On a serverless host, the agent must finish within the function's time limit. ## Create the chat component Create `app/chat.js`. The component gets the conversation's channel from the Ably client, creates the [chat transport](https://ably.com/docs/ai-transport/api/javascript/vercel/chat-transport.md) on the channel and passes the transport to `useChat` with the stored messages and `resume: true`. The Stop button appears only once the response is streaming, so the agent has already subscribed to cancel messages when the user presses it. The Stop button publishes a `cancel` message, and then stops the response in this tab: ### Javascript ``` 'use client'; // Client code. The browser fetches a token, so it never holds an API key. import { useEffect, useMemo, useState } from 'react'; import { useChat } from '@ai-sdk/react'; import { createChatTransport } from '@ably/ai-transport/vercel'; export function Chat({ client, chatId, initialMessages }) { const channel = client.channels.get(chatId); const transport = useMemo(() => createChatTransport({ channel }), [channel]); const { messages, sendMessage, status, stop, error } = useChat({ id: chatId, messages: initialMessages, transport, resume: true, }); // useChat does not close its transport. Closing the transport leaves the agent running. useEffect(() => () => transport.close(), [transport]); const [input, setInput] = useState(''); // Disable sending while this tab's response is in progress. const busy = status === 'submitted' || status === 'streaming'; const send = (e) => { e.preventDefault(); const text = input.trim(); if (!text || busy) return; setInput(''); void sendMessage({ text }); }; const stopResponse = () => { void channel.publish('cancel', {}); void stop(); }; return (
{messages.map((m) => (

{m.role}: {m.parts.map((p) => (p.type === 'text' ? p.text : '')).join('')}

))} {error &&

{error.message}

}
setInput(e.target.value)} /> {status === 'streaming' ? ( ) : ( )}
); } ```
## Wire it together Create `app/page.js`. The page creates the browser's Ably client, loads the stored conversation and shows the chat once both are ready, because `useChat` reads `messages` only when it starts: ### Javascript ``` 'use client'; // Client code. import { useEffect, useState } from 'react'; import { createClient } from '@ably/pubsub-device'; import { Chat } from './chat'; const chatId = 'conversations:demo'; export default function Page() { const [client, setClient] = useState(); const [initialMessages, setInitialMessages] = useState(); useEffect(() => { const realtime = createClient({ authUrl: '/api/auth/token' }); setClient(realtime); return () => realtime.close(); }, []); useEffect(() => { fetch(`/api/chat?chat=${encodeURIComponent(chatId)}`) .then((res) => res.json()) .then(({ messages }) => setInitialMessages(messages)); }, []); if (!client || !initialMessages) return null; return ; } ``` Create `app/layout.js`. Next.js needs a root layout to build the app: ### Javascript ``` export default function RootLayout({ children }) { return ( {children} ); } ``` ## Run the app Start the dev server: ### Shell ``` npm run dev ``` Open `http://localhost:3000` and send a message. While the response streams, reload the page, open the app in a second tab or press Stop. After the reload, the tab shows the response so far, and then the rest of it live. The second tab shows the response too. Pressing Stop stops the agent and ends the response in both tabs. > **Note** > > A tab does not show a message that another tab sends, or the response to it, until the tab reloads, unless the component adds a [channel subscription](https://ably.com/docs/ai-transport/frameworks/vercel-ai-sdk-ui.md#join). The agent's HTTP endpoint saves the conversation that each POST carries, so if a tab that missed another tab's message then sends one, this guide's store loses the missed message and its response. If you send a message while the previous response is still in progress, such as just after you stop a response, the agent's HTTP endpoint answers 409, and the chat shows the error. ## What happens when you send a message 1. `useChat` adds the user's message and calls the chat transport, which POSTs the conversation to the agent's HTTP endpoint. 2. The endpoint saves the conversation and returns a 202. The agent then runs, and publishes the user's message to the channel. 3. The agent pipes the model's output to the channel, and the chat transport passes each chunk to `useChat`, which builds the response. 4. The response ends with `finish`, `abort`, or `error`. `useChat` returns to `ready` after `finish` or `abort`, and to `error` after `error`. The agent saves the conversation when the response ends, unless the pipe failed and the `catch` block published the `error`. ## Understand the architecture `useChat` holds the messages and the status. The chat transport POSTs the conversation to your agent's HTTP endpoint, and passes `useChat` the chunks it reads from the channel. For what `useChat` does not cover, such as a turn started on another device, your code can use the channel directly. ## Explore next - [Multi-device and fan-out](https://ably.com/docs/ai-transport/conversation/multi-device.md): how every connected client receives the same messages. - [Vercel AI SDK UI](https://ably.com/docs/ai-transport/frameworks/vercel-ai-sdk-ui.md): what the chat transport adds to `useChat`, and what your code can build on the channel. - [Chat transport API reference](https://ably.com/docs/ai-transport/api/javascript/vercel/chat-transport.md): every option, and how the transport behaves. - [Get started with the Vercel AI SDK](https://ably.com/docs/ai-transport/get-started/vercel-ai-sdk.md): a chat app whose client code merges each response itself. - [Token streaming](https://ably.com/docs/ai-transport/conversation/token-streaming.md): how a response is appended to one message. - [Reconnection and recovery](https://ably.com/docs/ai-transport/conversation/reconnection-and-recovery.md): what happens when a client loses its connection. ## Related Topics - [Vercel AI SDK](https://ably.com/docs/ai-transport/get-started/vercel-ai-sdk.md): Build a streaming chat app with the Vercel AI SDK and Ably AI Transport. Responses reach every connected client, and a client that reloads reads the conversation from your database, then replays what arrived on the channel after it. - [OpenAI responses](https://ably.com/docs/ai-transport/get-started/openai.md): Build a streaming chat app with the OpenAI Responses API and Ably AI Transport. Responses reach every connected client, and a client that reloads reads the conversation from your database, then replays what arrived on the channel after it. - [Custom events](https://ably.com/docs/ai-transport/get-started/custom-events.md): Build a Next.js chat app with your custom events. Define your event format with defineCodec, stream it from the agent with an adaptor, and decode it in every subscribed client. ## Documentation Index To discover additional Ably documentation: 1. Fetch [llms.txt](https://ably.com/llms.txt) for the canonical list of available pages. 2. Identify relevant URLs from that index. 3. Fetch target pages as needed. Avoid using assumed or outdated documentation paths.