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

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-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, including how to scope capabilities to a single conversation channel.

The client below uses authUrl: '/auth' to fetch tokens from this endpoint.

Create the server route

Create app/api/chat/route.ts. The server publishes the user's message to an Ably channel, invokes the LLM, and streams tokens into the channel. The HTTP response returns immediately; tokens flow through the durable session.

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

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/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 });
  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 client uses Vercel's useChat hook with the AI Transport adapter. The transport subscribes to the Ably channel and syncs messages across every connected client.

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 { useChat } from '@ai-sdk/react';
import { useActiveTurns, useView } from '@ably/ai-transport/react';
import { useChatTransport, useMessageSync } from '@ably/ai-transport/vercel/react';
import { useState } from 'react';

export function Chat({ chatId }) {
  const [input, setInput] = useState('');

  const { chatTransport } = useChatTransport();
  const { messages, setMessages, sendMessage, stop } = useChat({
    id: chatId,
    transport: chatTransport,
  });

  useMessageSync({ setMessages });
  useView({ limit: 30 });

  const activeTurns = useActiveTurns();
  const isStreaming = activeTurns.size > 0;

  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();
          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. The Providers component sets up an authenticated Ably client using authUrl: '/auth'. The TransportProvider wires the channel and codec into AI Transport.

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

'use client';

import { useEffect, useState } 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 }) {
  const [client, setClient] = useState(null);

  useEffect(() => {
    const ably = new Ably.Realtime({ authUrl: '/auth' });
    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>
      <ChatTransportProvider channelName={chatId}>
        <Chat chatId={chatId} />
      </ChatTransportProvider>
    </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

  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.

This is a durable session. The HTTP request triggers the agent; all communication flows through the Ably channel.

Understand the architecture

Vercel AI SDK handles intelligence and UI. AI Transport handles what happens between the model and every device. See Vercel AI SDK UI for the client-side integration and Vercel AI SDK Core for the server-side integration.

Vercel built the ChatTransport interface as the extension point for custom transports. AI Transport implements ChatTransport, so you swap the transport layer without changing your application code:

JavaScript

1

2

3

4

5

6

7

// Before: default HTTP transport
const { messages } = useChat();

// After: Ably transport (everything else stays the same)
// Wrap your tree with <ChatTransportProvider channelName={chatId}> first.
const { chatTransport } = useChatTransport();
const { messages } = useChat({ transport: chatTransport });

Choose an integration path

Both paths use the same server code. The difference is client-side only.

The useChat path is the simplest. useChatTransport wraps the core transport for direct use with Vercel's useChat hook. useMessageSync pushes other clients' messages into useChat state. You get Vercel's message management with AI Transport's durable delivery. Use this path for the standard Vercel useChat developer experience with durable sessions added. This is the path the tutorial above follows.

The Core SDK path uses AI Transport's React hooks (useView, useTree, useCreateView) directly instead of useChat. The write operations (send, regenerate, edit, update) come back as methods on the ViewHandle returned by useView. This gives you full access to the conversation tree, branch navigation, split-pane views, and custom message construction. Use this path for branching UI, custom message rendering, or direct control over the conversation tree.

Explore next