### Shell
```
npm install @ably/ai-transport ably ai @ai-sdk/react @ai-sdk/anthropic next react react-dom
```
## Set up authentication
Create an auth endpoint at `/api/auth/token` that returns an Ably JWT to the client. The endpoint validates the user and signs a token with their client ID and the channel capabilities they need, as described in [Set up authentication](https://ably.com/docs/ai-transport/setup/authentication.md).
The client below uses `authUrl: '/api/auth/token'` to fetch tokens from this endpoint.
## Configure the channel rule
AI Transport streams each response by appending tokens to a single channel message. That requires the **Message annotations, updates, deletes, and appends** channel rule (`mutableMessages`) on the namespace your conversations live on.
In your Ably dashboard, enable **Message annotations, updates, deletes, and appends** on the `conversations` namespace, using the [dashboard, Control API, or CLI](https://ably.com/docs/ai-transport/setup/channel-rules.md).
## Create the agent route
On the agent, create `app/api/chat/route.ts`. The agent receives an [invocation](https://ably.com/docs/ai-transport/streaming/runs-and-steps.md#invocations), creates an [AgentSession](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md) pre-bound to the Vercel codec, starts a run, hydrates the conversation, pipes the LLM stream, and ends the run.
### Javascript
```
import { after } from 'next/server';
import { streamText, convertToModelMessages } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import * as Ably from 'ably';
import { Invocation } from '@ably/ai-transport';
import { createAgentSession, vercelRunOutcome } from '@ably/ai-transport/vercel';
const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY });
export async function POST(req) {
const invocation = Invocation.fromJSON(await req.json());
const session = createAgentSession({
client: ably,
channelName: invocation.sessionName,
});
await session.connect();
const run = session.createRun(invocation, {}, { signal: req.signal });
after(async () => {
try {
// Rebuild the conversation from run.view before run.start(): draining pages
// in this run's triggering input (otherwise run.start() awaits it live).
while (run.view.hasOlder()) {
await run.view.loadOlder();
}
const conversation = run.view.getMessages().map(({ message }) => message);
await run.start();
const result = streamText({
model: anthropic('claude-sonnet-4-20250514'),
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(conversation),
abortSignal: run.abortSignal,
});
const pipeResult = await run.pipe(result.toUIMessageStream());
const outcome = await vercelRunOutcome(pipeResult, result.finishReason);
if (outcome.reason === 'suspend') {
await run.suspend();
} else {
await run.end(outcome);
}
} catch (err) {
await run.end({ reason: 'error' });
throw err;
} finally {
session.end();
}
});
return Response.json({ runId: run.runId, invocationId: run.invocationId });
}
```
## Create the chat component
On the client, create `app/chat.tsx`. The component reads the chat transport from the nearest [`ChatTransportProvider`](https://ably.com/docs/ai-transport/api/react/vercel/chat-transport-provider.md) with [`useChatTransport`](https://ably.com/docs/ai-transport/api/react/vercel/use-chat-transport.md) and passes it to Vercel's `useChat` hook. The transport owns the agent-invocation POST; `useChat` calls into it as if it were the default HTTP transport. [`useMessageSync`](https://ably.com/docs/ai-transport/api/react/vercel/use-message-sync.md) feeds session updates back into `useChat`, so other clients see messages they did not publish.
### Javascript
```
'use client';
import { useState } from 'react';
import { useChat } from '@ai-sdk/react';
import { useChatTransport, useMessageSync } from '@ably/ai-transport/vercel/react';
export function Chat() {
const [input, setInput] = useState('');
const { session, chatTransport, chatTransportError } = useChatTransport();
const { messages, setMessages, sendMessage, status } = useChat({
transport: chatTransport,
});
useMessageSync({ setMessages });
if (chatTransportError) {
return Failed to connect: {chatTransportError.message};
}
const isStreaming = status === 'submitted' || status === 'streaming';
const stop = () => {
const activeRun = session.view.runs().find((run) => run.status === 'active');
if (activeRun) void session.cancel(activeRun.runId);
};
return (
{messages.map((msg) => (
{msg.role}:{' '}
{msg.parts.map((part, i) =>
part.type === 'text' ? {part.text} : null,
)}
))}
);
}
```
## Wire it together
On the client, create `app/page.tsx`. `Providers` sets up an authenticated Ably client. [`ChatTransportProvider`](https://ably.com/docs/ai-transport/api/react/vercel/chat-transport-provider.md) constructs the underlying [`ClientSession`](https://ably.com/docs/ai-transport/api/javascript/core/client-session.md) bound to the channel and the [`ChatTransport`](https://ably.com/docs/ai-transport/api/javascript/vercel/chat-transport.md) over it. It POSTs invocations to `/api/chat` by default, matching the agent route above; set `api` to point elsewhere.
Update `channelName` to match a namespace with the AIT [channel rules](https://ably.com/docs/ai-transport/setup/channel-rules.md) configured.
### Javascript
```
'use client';
import { useEffect, useState, type ReactNode } from 'react';
import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';
import { ChatTransportProvider } from '@ably/ai-transport/vercel/react';
import { Chat } from './chat';
function Providers({ children }: { children: ReactNode }) {
const [client, setClient] = useState(null);
useEffect(() => {
const ably = new Ably.Realtime({ authUrl: '/api/auth/token', clientId: 'user-abc' });
setClient(ably);
return () => ably.close();
}, []);
if (!client) return null;
return {children} ;
}
export default function Page() {
return (
);
}
```
Run `npm run dev` and open `http://localhost:3000`. Open a second tab to the same URL; both tabs share the same durable session.
## What happens when you send a message
1. `sendMessage({ text })` calls the `ChatTransport`'s `sendMessages`. Inside that call, the AI Transport client SDK generates `inputEventId` and the message's `codecMessageId`, sets them under `extras.ai.transport` on the message it publishes, and returns a `ClientRun` carrying the synchronous `inputCodecMessageId`. The `ChatTransport` then calls `clientRun.toInvocation().toJSON()` and POSTs the resulting `InvocationData` body (`{ inputEventId, sessionName }`) to your agent endpoint.
2. The agent route receives the POST, calls `Invocation.fromJSON`, creates an `AgentSession`, and starts a run. `session.createRun(invocation)` generates `runId` (for a fresh run) and `invocationId` (one per HTTP request); the agent sets both on every event it publishes and returns them on the response. `run.start()` waits until the trigger input event has been observed on the session, whether paged in from history (as the drain above does) or arriving live.
3. The agent streams the LLM response through `run.pipe()` to the session. Outputs carry `input-codec-message-id` so the client can correlate by an id it owned at send time.
4. Every client subscribed to the session decodes the streamed messages in realtime. `useChat` re-renders as `UIMessage` parts accumulate.
5. If a client disconnects mid-stream, Ably resumes the subscription from the last serial on reconnect; the session rehydrates without losing tokens.
## What the SDK holds for you here
The SDK provides four things in this app, and all four are yours to write if you adopt [streaming](https://ably.com/docs/ai-transport/streaming.md) alone:
| What | Where it lives here |
| --- | --- |
| The conversation | On the session, addressed by channel name. |
| Message state | `useMessageSync` merges the event stream into `UIMessage`s and keeps `useChat`'s list in step, including dropping the output of a superseded step attempt. |
| Hydration | A new tab or a reload rebuilds the conversation from channel history. |
| Branching | An edit or a regenerate forks the conversation tree and both versions stay navigable. |
Streaming keeps the run and step structure, cancellation, steering, and fan-out exactly as they are here. The four rows above become your code to write.
## Understand the architecture
Vercel AI SDK handles model orchestration and UI. AI Transport handles the durable session between agent and devices, through the [client-side UI integration](https://ably.com/docs/ai-transport/frameworks/vercel-ai-sdk-ui.md) and the [server-side Core integration](https://ably.com/docs/ai-transport/frameworks/vercel-ai-sdk-core.md).
## Explore next
- [Vercel AI SDK UI](https://ably.com/docs/ai-transport/frameworks/vercel-ai-sdk-ui.md): client-side integration with `useChat`.
- [Cancellation](https://ably.com/docs/ai-transport/streaming/cancellation.md): the stop button pattern and the agent-side `onCancel` authorisation hook.
- [Multi-device and fan-out](https://ably.com/docs/ai-transport/streaming/multi-device.md): open another tab to see realtime sync.
- [History and replay](https://ably.com/docs/ai-transport/streaming/history.md): load past conversation on mount.
- [Sessions](https://ably.com/docs/ai-transport/durable-sessions/sessions.md): durable sessions and how the conversation persists.
## Related Topics
- [Session useView](https://ably.com/docs/ai-transport/durable-sessions/quickstart-react-hooks.md): Build a streaming AI chat app on an AI Transport session with the useView hook. Full access to the conversation tree, branching, and pagination.
- [OpenAI](https://ably.com/docs/ai-transport/durable-sessions/quickstart-openai.md): Build a streaming AI chat app with the OpenAI Responses API and Ably AI Transport. The OpenAI codec streams model output over a durable session with multi-device sync and cancellation.
## 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.