Set up authentication
AI Transport authenticates through the auth you already have. Your server validates the user and signs a short-lived Ably token, and the browser fetches that token and refreshes it before it expires.
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.
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 JWT with the capabilities AI Transport needs. The endpoint signs the token with jsonwebtoken:
npm install jsonwebtoken1
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) {
return 'user-abc';
}
export async function GET(req) {
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 beneath conversations:, because a conversation's channel name (conversations:demo 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.
Scope capabilities
Capabilities are permissions on the Ably channel. The capability claim in the token lists which operations a user can perform on which channels:
| Feature | Required capabilities |
|---|---|
| Send user messages to the channel | publish |
| Receive streamed responses | subscribe |
| Replay history on load or after a discontinuity | subscribe, history |
| Show agent presence | subscribe on the client, presence on the agent |
| Read and write shared objects | object-subscribe, object-publish |
| All AI Transport features | publish, subscribe, history |
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 beneathconversations:.*: 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:
1
await realtimeClient.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
Construct an Ably Realtime client with authCallback. The SDK calls it on first auth and again whenever a refresh is needed:
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 on the client, hold the client in state, wrap the tree in AblyProvider, then in TransportProvider for the conversation's channel:
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 { TransportProvider, useTransport } from '@ably/ai-transport/react';
import { openai } from '@ably/ai-transport/openai';
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 (
<TransportProvider channelName={conversationId} codec={openai}>
<Chat />
</TransportProvider>
);
}
function Chat() {
const { transport, error } = useTransport();
// ...
}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:
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 }),
});
}Read next
- Configure the channel rule: the one-time channel configuration AI Transport requires.