# 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](https://ably.com/docs/ai-transport/features/cancellation.md#authorization) 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](#capabilities) AI Transport needs: ### Javascript ``` 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 { 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](https://ably.com/docs/ai-transport/troubleshooting.md#capability-mismatch) 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 the `conversations:` 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: ### Javascript ``` await realtimeClient.auth.authorize(); ``` To remove access immediately, [revoke issued tokens](https://ably.com/docs/auth/revocation.md). If your capability JSON is too large for a JWT claim, or it must stay confidential, use native [Ably Tokens](https://ably.com/docs/auth/token/ably-tokens.md) 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: ### Javascript ``` 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: ### Javascript ``` '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 {children}; } function App({ conversationId }) { return ( ); } 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: ### Javascript ``` 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`](https://ably.com/docs/ai-transport/api/react/vercel/chat-transport-provider.md) 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](https://ably.com/docs/ai-transport/getting-started/channel-rules.md): the one-time namespace configuration AI Transport requires. - [Core SDK getting started](https://ably.com/docs/ai-transport/getting-started/core-sdk.md): build a chat app on the auth set up here. - [Vercel AI SDK getting started](https://ably.com/docs/ai-transport/getting-started/vercel-ai-sdk.md): build a chat app using the Vercel wrapper. ## Related Topics - [Configure the channel rule](https://ably.com/docs/ai-transport/getting-started/channel-rules.md): Configure the channel rule AI Transport requires as part of getting started: Message annotations, updates, deletes, and appends on the channel namespace your conversations live on. ## Documentation Index To discover additional Ably documentation: 1. Fetch [llms.txt](https://ably.com/llms.txt) for the canonical list of available pages. 2. Identify relevant URLs from that index. 3. Fetch target pages as needed. Avoid using assumed or outdated documentation paths.