Get started with Vercel AI SDK
Build a working chat app with Vercel AI SDK and AI Transport. Tokens stream over a durable session, so the conversation survives reconnects and syncs across tabs.
What you build
A Next.js chat app where:
- Tokens stream from the model into a durable session, not the HTTP response.
- Closing a tab and reopening it resumes the in-progress response.
- A second tab on the same session sees the same conversation in real time.
- A stop button cancels the in-progress Run.
For direct access to the conversation tree, branching, and pagination, see Get started with the Core SDK.
Prerequisites
- Node.js 20 or later.
- An Ably account with an API key.
- An Anthropic API key, or any other model provider supported by Vercel AI SDK.
Install dependencies
npm install @ably/ai-transport ably ai @ai-sdk/react @ai-sdk/anthropic next react react-domSet 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. See Set up authentication for the full setup.
The client below uses authUrl: '/api/auth/token' to fetch tokens from this endpoint.
Create the agent route
Create app/api/chat/route.ts. The agent receives an Invocation, creates an AgentSession pre-bound to UIMessageCodec, starts a Run, hydrates the conversation, pipes the LLM stream, and ends the Run.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 {
await run.start();
await run.loadConversation();
const result = streamText({
model: anthropic('claude-sonnet-4-20250514'),
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(run.messages),
abortSignal: run.abortSignal,
});
const pipeResult = await run.pipe(result.toUIMessageStream());
const outcome = await vercelRunOutcome(pipeResult, result.finishReason);
if (outcome === 'suspend') {
await run.suspend();
} else {
await run.end(outcome);
}
} catch (err) {
await run.end('error');
throw err;
} finally {
session.close();
}
});
return Response.json({ runId: run.runId, invocationId: run.invocationId });
}Create the chat component
Create app/chat.tsx. The component uses Vercel's useChat hook with createChatTransport from @ably/ai-transport/vercel. The Vercel wrapper owns the agent-invocation POST; useChat calls into it as if it were the default HTTP transport.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
'use client';
import { useMemo, useState } from 'react';
import { useChat } from '@ai-sdk/react';
import { useClientSession } from '@ably/ai-transport/react';
import { createChatTransport } from '@ably/ai-transport/vercel';
export function Chat({ chatId }) {
const [input, setInput] = useState('');
const { session } = useClientSession();
const chatTransport = useMemo(() => createChatTransport(session), [session]);
const { messages, sendMessage, status, stop } = useChat({
id: chatId,
transport: chatTransport,
});
const isStreaming = status === 'submitted' || status === 'streaming';
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>{' '}
{msg.parts.map((part, i) =>
part.type === 'text' ? <span key={i}>{part.text}</span> : null,
)}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ text: input });
setInput('');
}}
>
<input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type a message..." />
{isStreaming ? (
<button type="button" onClick={stop}>Stop</button>
) : (
<button type="submit">Send</button>
)}
</form>
</div>
);
}Wire it together
Create app/page.tsx. Providers sets up an authenticated Ably client. ClientSessionProvider constructs the underlying ClientSession bound to the channel.
Update channelName to match a namespace with the AIT channel rules configured.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
'use client';
import { useEffect, useState } from 'react';
import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';
import { ClientSessionProvider } from '@ably/ai-transport/react';
import { UIMessageCodec } from '@ably/ai-transport/vercel';
import { Chat } from './chat';
function Providers({ children }) {
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 <AblyProvider client={client}>{children}</AblyProvider>;
}
export default function Page() {
const chatId = 'conversations:my-chat-session';
return (
<Providers>
<ClientSessionProvider channelName={chatId} codec={UIMessageCodec}>
<Chat chatId={chatId} />
</ClientSessionProvider>
</Providers>
);
}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 is happening
sendMessage({ text })calls theChatTransport'ssendMessages. Under the hood, the AI Transport client SDK mintsinputEventIdand the message'scodecMessageId, stamps them on the channel publish underextras.ai.transport, and returns anActiveRuncarrying the synchronousinputCodecMessageId. TheChatTransportthen callsactiveRun.toInvocation().toJSON()and POSTs the resultingInvocationDatabody ({ inputEventId, sessionName }) to your agent endpoint.- The agent route receives the POST, calls
Invocation.fromJSON, creates anAgentSession, and starts a Run.session.createRun(invocation)mintsrunId(for a fresh run) andinvocationId(one per HTTP request); the agent stamps both on every event it publishes and returns them on the response.run.start()waits for the trigger input event on the channel (rewind plus live). - The agent streams the LLM response through
run.pipe()to the channel. Outputs carryinput-codec-message-idso the client can correlate by an id it owned at send time. - Every client subscribed to the channel decodes the streamed messages in real time.
useChatre-renders asUIMessageparts accumulate. - If a client disconnects mid-stream, Ably resumes the subscription from the last serial on reconnect; the session rehydrates without losing tokens.
Understand the architecture
Vercel AI SDK handles model orchestration and UI. AI Transport handles the durable session between agent and devices. See Vercel AI SDK UI for the client-side integration and Vercel AI SDK Core for the server-side integration.
Explore next
- Vercel AI SDK UI: client-side integration with
useChat. - Cancellation: the stop button pattern and the agent-side
onCancelauthorisation hook. - Multi-device sessions: open another tab to see real-time sync.
- History and replay: load past conversation on mount.
- Sessions: durable sessions and how the conversation persists.