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.

Open in

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 useView hook, 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-dom

Set 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.

JavaScript

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:

JavaScript

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.

JavaScript

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

  1. The client sends the user message via HTTP POST to your API route.
  2. The server publishes the message to the Ably channel, invokes the LLM, and streams tokens to the channel.
  3. Every client subscribed to the channel receives tokens in real time.
  4. 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