# Get started with useView Build a chat app that reads a session directly through the useView hook. 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](https://ably.com/docs/ai-transport/durable-sessions/sessions.md) in realtime. - The client reads the conversation through the [`useView`](https://ably.com/docs/ai-transport/api/react/core/use-view.md) hook, with direct access to the [conversation tree](https://ably.com/docs/ai-transport/durable-sessions/conversation-tree.md). - Branching, edit, regenerate, and pagination are first-class. - A Stop button cancels the in-progress [run](https://ably.com/docs/ai-transport/streaming/runs-and-steps.md). To use AI Transport with Vercel's `useChat` for message management, see [Get started with Vercel AI SDK](https://ably.com/docs/ai-transport/durable-sessions/quickstart-use-chat.md). ## Prerequisites - Node.js 22 or later. - An [Ably account](https://ably.com/sign-up) with an API key. - An Anthropic API key, or any other model provider you prefer. ## Install dependencies ### Shell ``` npm install @ably/ai-transport ably ai @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), starts a [run](https://ably.com/docs/ai-transport/streaming/runs-and-steps.md), 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 { createAgentSession, Invocation } from '@ably/ai-transport'; import { createUIMessageSessionCodec } 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, codec: createUIMessageSessionCodec(), }); 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 { reason } = await run.pipe(result.toUIMessageStream()); await run.end({ reason }); } 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 uses [`useView`](https://ably.com/docs/ai-transport/api/react/core/use-view.md) directly. It returns the visible messages, write methods, branch navigation, and pagination. Because the SDK doesn't POST to the agent endpoint itself, the component does it after `view.send` resolves. ### Javascript ``` 'use client'; import { useState } from 'react'; import { useClientSession, useView } from '@ably/ai-transport/react'; import { createUIMessageSessionCodec } from '@ably/ai-transport/vercel'; async function wakeAgent(run) { const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(run.toInvocation().toJSON()), }); return res.json(); } export function Chat() { const [input, setInput] = useState(''); const { session } = useClientSession(); const view = useView({ limit: 30 }); const { messages, runOf } = view; // `'active'`, `'suspended'`, and `'error'` keep the Stop button visible // so a stuck run can still be cancelled; `'complete'` and `'cancelled'` // are terminal. const latestRun = runOf(messages.at(-1)?.codecMessageId ?? ''); const isStreaming = latestRun !== undefined && latestRun.status !== 'complete' && latestRun.status !== 'cancelled'; const handleSubmit = async (e) => { e.preventDefault(); if (!input.trim()) return; const text = input; setInput(''); const run = await view.send(createUIMessageSessionCodec().createUserMessage({ id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text }], })); await wakeAgent(run); }; const stop = async () => { if (latestRun) await session.cancel(latestRun.runId); }; return (
{view.hasOlder && ( )} {messages.map(({ codecMessageId, message }) => (
{message.role}:{' '} {message.parts.map((part, i) => part.type === 'text' ? {part.text} : null, )}
))}
setInput(e.target.value)} placeholder="Type a message..." /> {isStreaming ? ( ) : ( )}
); } ```
## Wire it together On the client, create `app/page.tsx`. `Providers` sets up an authenticated Ably client. [`ClientSessionProvider`](https://ably.com/docs/ai-transport/api/react/core/providers.md) wires the session and codec into AI Transport. 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 } from 'react'; import * as Ably from 'ably'; import { AblyProvider } from 'ably/react'; import { ClientSessionProvider } from '@ably/ai-transport/react'; import { createUIMessageSessionCodec } from '@ably/ai-transport/vercel'; import { Chat } from './chat'; // A stable codec instance held at module scope, passed to the provider below. const uiMessageCodec = createUIMessageSessionCodec(); function Providers({ children }) { const [client, setClient] = useState(null); useEffect(() => { const ably = new Ably.Realtime({ authUrl: '/api/auth/token', clientId: 'user' }); setClient(ably); return () => ably.close(); }, []); if (!client) return null; return {children}; } export default function Page() { const channelName = 'conversations:my-chat-session'; return ( ); } ``` Run `npm run dev` and open `http://localhost:3000`. Open a second tab to the same URL to see both tabs share the same session. ## What happens when you send a message 1. The user types a message. `view.send(...)` generates `inputEventId` and the message's `codecMessageId`, publishes the user input on the session carrying those ids under `extras.ai.transport`, and returns a `ClientRun` carrying the `inputCodecMessageId` the client owns synchronously. The SDK does not POST to your agent endpoint itself. 2. Your client code calls `clientRun.toInvocation().toJSON()` to get an [`InvocationData`](https://ably.com/docs/ai-transport/streaming/runs-and-steps.md#invocations) body (`{ inputEventId, sessionName }`), and POSTs it to your agent endpoint. 3. The agent endpoint receives the POST, calls `Invocation.fromJSON`, creates an `AgentSession`, and starts a `Run` from the invocation. `session.createRun(invocation)` generates a fresh `runId` (for a fresh run) and a fresh `invocationId`; the agent returns both on the response so callers can observe them. `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. 4. The agent streams the LLM response through `run.pipe(...)` to the session. Every output carries `input-codec-message-id` so the client can correlate by an id it owned at send time. 5. Every client subscribed to the session receives the streamed messages in realtime. `useView` re-renders as the visible run's projection updates. 6. If a client disconnects mid-stream, Ably resumes the subscription from the last serial on reconnect; the SDK rehydrates the view automatically. `useView` subscribes to the conversation tree and returns the visible messages along the currently selected branch. The hook exposes `messages`, `hasOlder`, `loading`, `loadError`, `loadOlder`, `runOf`, `run`, `runs`, `branchSelection`, `send`, `regenerate`, and `edit`. Switch siblings through the `branchSelection` handle's `select`. It is the React equivalent of the [`ClientView` interface](https://ably.com/docs/ai-transport/api/javascript/core/client-session.md#properties). ## What the SDK holds for you The SDK provides four things in this app: | What | Where it lives here | | --- | --- | | The conversation | On the session, addressed by channel name. | | Message state | `useView` returns the messages already merged from the event stream, with a superseded step attempt's output dropped. | | Hydration | `useView` loads history from the channel on mount, and `loadOlder` pages further back. | | Branching | `edit` and `regenerate` fork the tree, and `branchSelection` moves between siblings. | ## Explore next - [Branching, edit, and regenerate](https://ably.com/docs/ai-transport/durable-sessions/branching.md): fork the conversation and navigate alternative branches with `view.regenerate`, `view.edit`, and the `view.branchSelection` handle's `select`. - [History and replay](https://ably.com/docs/ai-transport/streaming/history.md): `useView` loads history on mount via the `limit` option. - [Cancellation](https://ably.com/docs/ai-transport/streaming/cancellation.md): the Stop button pattern and the agent-side `onCancel` authorisation hook. - [Runs](https://ably.com/docs/ai-transport/streaming/runs-and-steps.md): the lifecycle primitive each turn becomes. - [React API reference](https://ably.com/docs/ai-transport/api/react/core/providers.md): providers and the per-hook pages. ## Related Topics - [Vercel useChat](https://ably.com/docs/ai-transport/durable-sessions/quickstart-use-chat.md): Build a streaming AI chat app with Vercel AI SDK and Ably AI Transport in a few minutes. Durable sessions, multi-device sync, and cancellation out of the box. - [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.