Set up authentication

AI Transport authenticates through your existing auth: your server validates the user and signs an Ably token, and the browser's Ably client fetches it through `authCallback`and refreshes it before expiry.

AI Transport reuses the authentication you already have. Your server validates the user and signs a short-lived Ably JWT, scoped to the channels that user can reach. The browser's Ably client fetches that token through an authCallback and refreshes it before it expires, so the connection stays authenticated for the whole conversation. The steps below wire up the server endpoint, the client, and the separate POST that wakes the agent.

Sign tokens on the server

Create an endpoint that authenticates the user and returns a short-lived Ably JWT with the capabilities AI Transport needs:

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

import jwt from 'jsonwebtoken';

// Replace with your app's auth: validate the request (session cookie, bearer
// token) and return the authenticated user's id.
async function authenticateUser(req: Request): Promise<string> {
  return 'user-abc';
}

export async function GET(req: Request) {
  const apiKey = process.env.ABLY_API_KEY;
  if (!apiKey) {
    return new Response('ABLY_API_KEY is not set', { status: 500 });
  }
  const [keyName, keySecret] = apiKey.split(':');

  const userId = await authenticateUser(req);

  const ablyJwt = jwt.sign(
    {
      'x-ably-capability': JSON.stringify({
        'conversations:*': ['publish', 'subscribe', 'history'],
      }),
      'x-ably-clientId': userId,
    },
    keySecret,
    { algorithm: 'HS256', keyid: keyName, expiresIn: '1h' },
  );

  return new Response(ablyJwt, { headers: { 'Content-Type': 'text/plain' } });
}

The capability grants conversations:*, every channel in the conversations namespace, because a session's channel name (conversations:my-chat-session in these guides) is not known when the token is signed. The x-ably-clientId claim binds the token to a specific user identity that the Ably service verifies on every publish. In production, scope the capability more tightly to the channels each user may access.

Fetch tokens from the client

Construct an Ably Realtime client with authCallback. The SDK calls it on first auth and again whenever a refresh is needed:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

import * as Ably from 'ably';

const realtimeClient = new Ably.Realtime({
  authCallback: async (tokenParams, callback) => {
    try {
      const response = await fetch('/api/auth/token', { credentials: 'include' });
      if (!response.ok) throw new Error('Auth failed');
      const jwt = await response.text();
      callback(null, jwt);
    } catch (error) {
      callback(error, null);
    }
  },
});

Wire it into the React provider stack

In a React app, hold the client in state, wrap the tree in AblyProvider, then in ClientSessionProvider for the channel:

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

'use client'

import { useEffect, useState } from 'react';
import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';
import { ClientSessionProvider, useClientSession } from '@ably/ai-transport/react';
import { UIMessageCodec } from '@ably/ai-transport/vercel';

export function Providers({ children }) {
  const [client, setClient] = useState(null);

  useEffect(() => {
    const ably = new Ably.Realtime({
      authCallback: async (_tokenParams, callback) => {
        try {
          const response = await fetch('/api/auth/token', { credentials: 'include' });
          callback(null, await response.text());
        } catch (err) {
          callback(err instanceof Error ? err.message : String(err), null);
        }
      },
    });
    setClient(ably);
    return () => ably.close();
  }, []);

  if (!client) return null;
  return <AblyProvider client={client}>{children}</AblyProvider>;
}

function App({ conversationId }) {
  return (
    <ClientSessionProvider channelName={conversationId} codec={UIMessageCodec}>
      <Chat />
    </ClientSessionProvider>
  );
}

function Chat() {
  const { session, sessionError } = useClientSession();
  // ...
}

Authenticate the agent POST

The application's POST that wakes the agent is a separate HTTP request from the channel auth. Authenticate it however you normally do (session cookie, bearer token, signed header) when you call fetch on the core flow:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

async function wakeAgent(run) {
  await fetch('/api/chat', {
    method: 'POST',
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${await getAccessToken()}`,
    },
    body: JSON.stringify(run.toInvocation().toJSON()),
  });
}

For the Vercel flow, ChatTransportProvider accepts a credentials prop and a chatOptions.prepareSendMessagesRequest hook that returns { body?, headers? } per request. Use the hook to attach auth headers to every invocation POST it makes for you.