From an empty project to a persisted, reported and titled conversation in the dashboard, and what to check when the first turn does not show up.
This guide takes the shortest path: an anonymous session, the AI SDK runtime and the assistant-ui components. The other guides swap the runtime; the cloud side stays the same.
Create a project
Sign in at cloud.assistant-ui.com and create a project. Copy its Frontend API URL from Settings › General; it looks like https://proj-<id>.assistant-api.com. A new project accepts anonymous sessions from any origin, so nothing else needs to be configured for a first run.
Configure the environment
NEXT_PUBLIC_ASSISTANT_BASE_URL=https://proj-<id>.assistant-api.com
OPENAI_API_KEY=sk-…On Next.js, the React runtimes create an anonymous cloud client from this variable when you pass no cloud. Vite, React Router and TanStack Start expose only VITE_ variables to the browser, so name it VITE_ASSISTANT_BASE_URL there.
EXPO_PUBLIC_ASSISTANT_BASE_URL=https://proj-<id>.assistant-api.com
EXPO_PUBLIC_API_URL=https://your-app.example.comThe second variable names the server that hosts your chat route, since it is not on the app's own origin.
ASSISTANT_BASE_URL=https://proj-<id>.assistant-api.com
API_URL=https://your-app.example.comThe second variable names the server that hosts your chat route.
Install and wire the runtime
npm install @assistant-ui/react @assistant-ui/ai-sdk ai @ai-sdk/openai"use client";
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/ai-sdk";
import { ThreadList } from "@/components/assistant-ui/elements/thread-list.aui";
import { Thread } from "@/components/assistant-ui/elements/thread.aui";
export default function ChatPage() {
const runtime = useChatRuntime();
return (
<AssistantRuntimeProvider runtime={runtime}>
<div className="grid h-dvh grid-cols-[240px_1fr]">
<ThreadList />
<Thread />
</div>
</AssistantRuntimeProvider>
);
}useChatRuntime() reads the base URL from the environment on Next.js and posts chat requests to /api/chat, the AI SDK route you already have. On Vite, React Router or TanStack Start, create the client once, outside the component, and pass it as useChatRuntime({ cloud }):
import { AssistantCloud } from "@assistant-ui/react";
const cloud = new AssistantCloud({
baseUrl: import.meta.env.VITE_ASSISTANT_BASE_URL,
anonymous: true,
});npx expo install @assistant-ui/react-native @assistant-ui/ai-sdk assistant-cloudimport { useMemo } from "react";
import { View } from "react-native";
import { AssistantRuntimeProvider } from "@assistant-ui/react-native";
import { AssistantCloud } from "assistant-cloud";
import { AssistantChatTransport, useChatRuntime } from "@assistant-ui/ai-sdk";
import { Thread } from "@/components/assistant-ui/elements/thread.aui";
export default function ChatScreen() {
const cloud = useMemo(
() =>
new AssistantCloud({
baseUrl: process.env.EXPO_PUBLIC_ASSISTANT_BASE_URL!,
anonymous: true,
}),
[],
);
const runtime = useChatRuntime({
cloud,
transport: new AssistantChatTransport({
api: `${process.env.EXPO_PUBLIC_API_URL}/api/chat`,
}),
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<View style={{ flex: 1 }}>
<Thread />
</View>
</AssistantRuntimeProvider>
);
}There is no localStorage on this platform, so an anonymous identity lasts one launch. Give users a lasting identity with an auth provider token before shipping.
npm install @assistant-ui/react-ink @assistant-ui/ai-sdk assistant-cloud ink reactimport { useMemo } from "react";
import { Box } from "ink";
import { AssistantRuntimeProvider } from "@assistant-ui/react-ink";
import { AssistantCloud } from "assistant-cloud";
import { AssistantChatTransport, useChatRuntime } from "@assistant-ui/ai-sdk";
import { Thread } from "./components/thread.js";
export function Chat() {
const cloud = useMemo(
() =>
new AssistantCloud({
baseUrl: process.env.ASSISTANT_BASE_URL!,
anonymous: true,
}),
[],
);
const runtime = useChatRuntime({
cloud,
transport: new AssistantChatTransport({
api: `${process.env.API_URL}/api/chat`,
}),
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<Box flexDirection="column">
<Thread />
</Box>
</AssistantRuntimeProvider>
);
}There is no localStorage in a terminal, so an anonymous identity lasts one process. Give users a lasting identity with an auth provider token before shipping.
Report model and usage
The browser cannot see which model answered or how many tokens it used. Add a messageMetadata callback to your route so the run report carries them:
import { convertToModelMessages, streamText } from "ai";
import { openai } from "@ai-sdk/openai";
export async function POST(req: Request) {
const { messages } = await req.json();
const model = openai("gpt-6-luna");
const result = streamText({
model,
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse({
messageMetadata: ({ part }) => {
if (part.type === "finish") {
return { usage: part.totalUsage, finishReason: part.finishReason };
}
if (part.type === "finish-step") {
return { modelId: part.response.modelId, provider: model.provider };
}
return undefined;
},
});
}Send a message and look
Send one message and open the dashboard. This is what the first turn leaves behind, and where to find it:
| In the dashboard | What you see | Written by |
|---|---|---|
| Threads | One thread for the anonymous visitor, with the two messages rendered on its page | The runtime, as the response settles |
| Threads, the thread's title | A short title in place of Untitled, as the response starts | The cloud, from the first message, streamed back to the runtime's own request |
| Runs | One completed run with its duration, time to first token, tokens, model and cost | The runtime's report, with the model and usage from the route |
| Engagement | One send, and the thread's satisfaction reading once a second turn or a copy happens | The runtime's events |
| Users | One anonymous user, active this month | The user's first message |
The app shows the title as soon as the model writes it; Thread titles explains how.
When the first turn does not show up
| What you see | Why | What to do |
|---|---|---|
| No thread appears | No cloud client was created, so the thread list stayed in memory: the environment variable is missing, or the app is not on Next.js and passes no cloud; or the browser's origin is refused, so every cloud request fails | Set NEXT_PUBLIC_ASSISTANT_BASE_URL and restart the dev server, or on Vite, React Router and TanStack Start pass the client built from VITE_ASSISTANT_BASE_URL; check Settings › Access and the browser console for a CORS refusal |
| The thread exists but stays Untitled | The title attempt failed, for instance because the title provider refused the call; the hourly sweep retries it up to three times | Open the thread in the dashboard: the Title row of its Details rail names the error. Fix the provider under Settings › Thread titles |
| The run reads No model reported and has no cost | The route did not return modelId and usage in its metadata | Add the messageMetadata callback from the step above |
| A 402 in the console | The project's plan has reached its active user cap for the month | See Plans and pricing |
Next
- Give users an identity of their own and lock down origins: Authentication.
- Join your server's spans to the browser's report: Traces.
- Read what every field of a report means: Run reports.
- Start from a template instead: cloud with anonymous sessions, or cloud-clerk with Clerk sign in.