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. The steps below wire up the server endpoint, the client, and the separate POST that wakes the agent.
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 trigger a run, and those are whatever your application already uses. Who can stop a run that is already in flight is a third question, and the cancel authorisation hook on the agent answers it.
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:
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.
Scope capabilities
Capabilities are permissions on the Ably channel. The capability claim in the token names which operations a user can perform on which channels:
| Feature | Required capabilities |
|---|---|
| Send user messages to the channel | publish |
| Receive streamed tokens | subscribe |
| Replay history on reconnect | subscribe, history |
| Cancel a run | publish |
| 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 in theconversations:namespace.*: all channels.
Scope tokens to the smallest channel set the user needs. A token for a user who is in one conversation should name that conversation rather than the whole namespace.
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 ClientSessionProvider for the session:
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
'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 { createUIMessageCodec } from '@ably/ai-transport/vercel';
// A stable codec instance (hold it at module scope or via useMemo).
const uiMessageCodec = createUIMessageCodec();
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. On the client, authenticate it however you normally do (session cookie, bearer token, signed header) when you call fetch on the core flow:
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.
Read next
- Configure the channel rule: the one-time namespace configuration AI Transport requires.
- Core SDK getting started: build a chat app on the auth set up here.
- Vercel AI SDK getting started: build a chat app using the Vercel wrapper.