Get started with the Core SDK
Build a chat app using AI Transport's core React hooks directly. You get the conversation tree, branch navigation, and pagination without going through a framework wrapper.
What you build
A Next.js chat app where:
- Tokens stream from the model into a durable session.
- The client reads the conversation through AI Transport's
useViewhook, with direct access to the conversation tree. - Branching, edit, regenerate, and pagination are first-class.
- A stop button cancels the in-progress turn.
To use AI Transport with Vercel's useChat for message management, see Get started with Vercel AI SDK.
Prerequisites
- Node.js 20 or later.
- An Ably account with an API key.
- An Anthropic API key, or any other model provider you prefer.
Install dependencies
npm install @ably/ai-transport ably ai @ai-sdk/anthropic next react react-domSet up authentication
Create an auth endpoint at /api/auth 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 authentication for the full setup.
The client below uses authUrl: '/auth' to fetch tokens from this endpoint.
Create the server route
Create app/api/chat/route.ts. The server creates a turn, publishes the user message, streams the LLM response through the Ably channel, and returns immediately. streamText orchestrates the model, and UIMessageCodec encodes its output for Ably. Swap either component if your stack is different.
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
import { after } from 'next/server';
import { streamText, convertToModelMessages } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import Ably from 'ably';
import { createServerTransport } from '@ably/ai-transport';
import { UIMessageCodec } from '@ably/ai-transport/vercel';
const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY });
export async function POST(req) {
const { messages, history, id, turnId, clientId, forkOf, parent } = await req.json();
const channel = ably.channels.get(id);
const transport = createServerTransport({ channel, codec: UIMessageCodec });
const turn = transport.newTurn({ turnId, clientId, parent, forkOf });
await turn.start();
if (messages.length > 0) {
await turn.addMessages(messages, { clientId });
}
const allMessages = [
...(history ?? []).map((h) => h.message),
...messages.map((m) => m.message),
];
const result = streamText({
model: anthropic('claude-sonnet-4-20250514'),
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(allMessages),
abortSignal: turn.abortSignal,
});
after(async () => {
const { reason } = await turn.streamResponse(result.toUIMessageStream());
await turn.end(reason);
transport.close();
});
return new Response(null, { status: 200 });
}Create the chat component
Create app/chat.tsx. The component uses AI Transport's core hooks directly. useView returns the visible messages, a send function, and pagination:
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
'use client';
import { useClientTransport, useActiveTurns, useView } from '@ably/ai-transport/react';
import { useState } from 'react';
export function Chat({ chatId }) {
const [input, setInput] = useState('');
const { transport } = useClientTransport();
const { messages, send, hasOlder, loadOlder } = useView({ limit: 30 });
const activeTurns = useActiveTurns();
const isStreaming = activeTurns.size > 0;
const handleSubmit = async (e) => {
e.preventDefault();
if (!input.trim()) return;
const text = input;
setInput('');
await send({
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text }],
});
};
return (
<div>
{hasOlder && (
<button type="button" onClick={loadOlder}>Load older messages</button>
)}
{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={handleSubmit}>
<input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type a message..." />
{isStreaming ? (
<button type="button" onClick={() => transport.cancel()}>Stop</button>
) : (
<button type="submit">Send</button>
)}
</form>
</div>
);
}Wire it together
Create app/page.tsx. Providers sets up an authenticated Ably client. TransportProvider wires the channel and codec into AI Transport. UIMessageCodec matches the server's output format; supply a different codec if your model layer produces different events.
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 { TransportProvider } 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: '/auth', clientId: 'user' });
setClient(ably);
return () => ably.close();
}, []);
if (!client) return null;
return <AblyProvider client={client}>{children}</AblyProvider>;
}
export default function Page() {
const chatId = 'my-chat-session';
return (
<Providers>
<TransportProvider channelName={chatId} codec={UIMessageCodec}>
<Chat chatId={chatId} />
</TransportProvider>
</Providers>
);
}Run npm run dev and open http://localhost:3000. Open a second tab to the same URL to see both tabs share the same durable session.
What is happening
- The client sends the user message via HTTP POST to your API route.
- The server publishes the message to the Ably channel, invokes the LLM, and streams tokens to the channel.
- Every client subscribed to the channel receives tokens in real time.
- If a client disconnects, it reconnects automatically and resumes from where it left off.
useView subscribes to the conversation tree and returns the visible messages along the currently selected branch. You have direct access to the tree: branching, pagination, and sibling navigation.
Explore next
- Conversation branching: use
useView'sgetSiblingsandselectto navigate branches. - Conversation branching: use
useView'sedit,regenerate, andselectto fork and navigate conversations. - History and replay:
useViewloads history on mount via thelimitoption. - Sessions: durable sessions and how the conversation persists.
- React API reference: providers and the per-hook pages.