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.

This page is the practical setup. For the conceptual model (the three auth layers, capabilities, token lifecycle, cancel authorisation), see the authentication concept.

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

import jwt from 'jsonwebtoken';

const [keyName, keySecret] = process.env.ABLY_API_KEY.split(':');

export async function GET(req) {
  const userId = await authenticateUser(req);

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

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

Scope the capability to the channel namespace the user is allowed to access, conversations:${userId} here. The x-ably-clientId claim binds the token to a specific user identity that the Ably service verifies on every publish.

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.