Set up authentication

Use your own endpoints to authenticate users and provide them with a token they can use for Ably operations like subscribe and publish. The token is short-lived and grants each user access only to the channels you allow.

AI Transport reuses the authentication you already have. Your server validates the user and signs a short-lived Ably token request, scoped to the channels that user can reach. The browser's Ably client fetches that token through an authUrl and refreshes it before it expires, so the connection stays authenticated for the whole conversation.

Two separate credentials are involved. The Ably token controls what a client can do on the channel. The credentials on the POST that wakes the agent control who can start a response, and those are whatever your application already uses.

Sign tokens on the server

On the server, create an endpoint that authenticates the user and returns a short-lived Ably token request with the capabilities AI Transport needs. The endpoint signs the token request with createTokenRequest from @ably/pubsub-server:

npm install @ably/pubsub-server
JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

import { createHttpClient } from '@ably/pubsub-server';

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

const ably = createHttpClient({ key: process.env.ABLY_API_KEY });

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

  const tokenRequest = await ably.auth.createTokenRequest({
    clientId: userId,
    capability: { 'conversations:*': ['subscribe', 'history'] },
  });

  return Response.json(tokenRequest);
}

The capability grants conversations:*, every channel beneath conversations:, because a conversation's channel name (conversations:demo in these guides) is not known when the token is signed. The clientId 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.

Scope capabilities

Capabilities are permissions on the Ably channel. The capability in the token lists which operations a user can perform on which channels:

FeatureRequired capabilities
Publish user messages to the channelpublish on the agent
Receive streamed responsessubscribe
Replay history on load or after a discontinuitysubscribe, history
Show agent presencesubscribe on the client, presence on the agent
Read and write shared objectsobject-subscribe, object-publish
Use the chat transport with useChatsubscribe and history, and publish if the client publishes a cancel message

A token missing a capability fails when the operation runs. The client constructs and connects successfully, so the error appears further into the flow than the mistake that caused it. This is one of the most common setup problems, and troubleshooting covers how to spot it.

Capabilities are keyed by channel name pattern, so the same token grants different operations on different channels:

  • my-conversation: a specific conversation channel.
  • conversations:*: all channels beneath conversations:.
  • *: all channels.

Scope tokens to the smallest channel set the user needs. A token for a user who is in one conversation should list that conversation's channel rather than every conversation.

Refresh and revoke tokens

The SDK refreshes tokens for you when you use authCallback or authUrl. It fetches a new token before the current one expires.

To change a user's capabilities mid-session, issue a new token from your auth server and re-authenticate the client:

JavaScript

1

await client.auth.authorize();

To remove access immediately, revoke issued tokens. If your capability JSON is too large for a JWT claim, or it must stay confidential, use native Ably Tokens instead.

Fetch tokens from the client

Create a client with createClient from @ably/pubsub-device, and set authUrl to your token endpoint. The SDK calls it on first auth and again whenever a refresh is needed:

npm install @ably/pubsub-device
JavaScript

1

2

3

import { createClient } from '@ably/pubsub-device';

const client = createClient({ authUrl: '/auth' });

Use the client in React

In a React app, create the client in an effect, and pass it to PubSubClientProvider from @ably/pubsub-device/react. Every component under the provider uses this client, which fetches its tokens from your /auth endpoint. The effect closes the client when the component unmounts.

This client code creates the client and wraps the component tree in the provider:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

'use client';

import { useEffect, useState } from 'react';
import { createClient } from '@ably/pubsub-device';
import { PubSubClientProvider } from '@ably/pubsub-device/react';

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

  useEffect(() => {
    const pubsubClient = createClient({ authUrl: '/auth' });
    setClient(pubsubClient);
    return () => pubsubClient.close();
  }, []);

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

Under the provider, your chat component subscribes a decoder to the conversation's channel and then attaches the channel. With useChat, the component passes the conversation's channel to createChatTransport instead.

Authenticate the agent POST

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

JavaScript

1

2

3

4

5

6

7

8

9

10

11

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

With useChat, the chat transport sends this POST. The browser sends a session cookie with the POST if the endpoint is on the same origin. For other credentials, pass headers with every call that sends a message: in the options of sendMessage() and regenerate(), and in the options property of addToolOutput() and addToolApprovalResponse(). The transport sends the headers with each request:

JavaScript

1

2

// Client code. getAccessToken() is your app's function that returns the user's access token.
await sendMessage({ text }, { headers: { Authorization: `Bearer ${await getAccessToken()}` } });

A turn that useChat sends automatically after a response that resume: true resumed does not include headers, so use a cookie if a resumed response can run a client-side tool, or pass headers to resumeStream() if your code resumes the response.