
Assistant-UI cloud-clerk template: how do I wire Clerk auth so each user only sees their own chat history?
Most teams using the Assistant-UI cloud-clerk template want one simple guarantee: when a user signs in with Clerk, they should only ever see their own chat history in Assistant UI Cloud—not anyone else’s. The good news is that Clerk and Assistant UI Cloud are designed to work together cleanly; you just need to wire the user identity through your API and queries in a consistent way.
Below is a practical, step-by-step guide to wiring Clerk auth so that each user only sees their own chat history, plus some patterns and checks to keep things secure and maintainable as your app grows.
What the cloud-clerk template gives you out of the box
The Assistant-UI cloud-clerk template typically includes:
- Clerk for authentication (with
<ClerkProvider>,<SignedIn>, etc.). - Assistant-UI as the React toolkit that:
- Renders a ChatGPT-style UI.
- Manages streaming, retries, and multi-turn state.
- Assistant UI Cloud for:
- Persisting chat threads so sessions survive refreshes.
- Building up long-term context over time.
- A backend route or “chat endpoint” that:
- Accepts messages.
- Calls your LLM provider (e.g., Vercel AI SDK, LangChain, OpenAI).
- Persists chats to Assistant UI Cloud.
Your job is to ensure the Clerk user ID (or a similar stable identifier) is:
- Required for every chat API call.
- Used when saving chat threads to Assistant UI Cloud.
- Used when fetching lists of threads or loading a specific thread.
- Verified on the server side so users cannot impersonate others.
Core concept: tie every chat thread to a Clerk user ID
The key principle:
Every persisted chat thread in Assistant UI Cloud must be associated with exactly one Clerk user (
userId), and all read/write operations must be filtered by thatuserId.
In practice, this means:
- When creating a new thread, you store
userIdin the thread’s metadata. - When listing threads, you only fetch threads where
userIdmatches the authenticated user. - When loading a single thread, you confirm that thread’s
userIdmatches the current user—otherwise you return 404 or 403.
Step 1: Get the authenticated user in your backend routes
Wherever your cloud-clerk template defines the API route for chat, you’ll want to:
- Use Clerk’s server-side helpers (e.g.,
getAuth,auth,currentUserdepending on framework). - Throw or return an error if the user is not authenticated.
- Extract the
userIdand pass it down into your Assistant UI Cloud logic.
Example (Next.js App Router + Clerk middleware)
// app/api/chat/route.ts
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
// import your assistant-ui cloud client
import { assistantClient } from "@/lib/assistantClient";
export async function POST(req: Request) {
const { userId } = auth();
if (!userId) {
return new NextResponse("Unauthorized", { status: 401 });
}
const body = await req.json();
const { message, threadId } = body;
// Pass userId down into your assistant-ui cloud logic
const result = await assistantClient.handleMessage({
message,
threadId,
userId,
});
return NextResponse.json(result);
}
The most important piece is that you never trust a userId sent from the client. Always derive it from Clerk on the server.
Step 2: Store userId in your Assistant UI Cloud thread metadata
Assistant UI Cloud stores threads and messages for you. To enforce per-user visibility, you should save the userId as part of every thread’s metadata.
The pattern:
- On thread creation, attach
userIdto the thread metadata. - On subsequent messages, fetch the thread, check that
userIdmatches, then append messages.
Example: creating a new thread
// lib/assistantClient.ts
import { createAssistantClient } from "@assistant-ui/cloud"; // adjust import to your setup
export const assistantClient = createAssistantClient({
// your config here
});
export async function handleMessage({
message,
threadId,
userId,
}: {
message: string;
threadId?: string;
userId: string;
}) {
if (!threadId) {
// New thread: attach userId to metadata
const thread = await assistantClient.threads.create({
messages: [
{ role: "user", content: message },
],
metadata: {
userId,
},
});
return { threadId: thread.id, messages: thread.messages };
}
// Existing thread: validate ownership first
const thread = await assistantClient.threads.get(threadId);
if (!thread || thread.metadata?.userId !== userId) {
throw new Error("Unauthorized access to thread");
}
const updated = await assistantClient.threads.appendMessage(threadId, {
role: "user",
content: message,
});
return { threadId: updated.id, messages: updated.messages };
}
This ensures every thread is permanently linked to the Clerk user.
Step 3: Only list chat history for the authenticated user
When you show a list of previous chats (e.g., a sidebar with conversation history), you should:
- Hit a backend endpoint that:
- Authenticates the user via Clerk.
- Queries Assistant UI Cloud with a filter on
metadata.userId.
- Return only the threads that match the authenticated
userId.
Example: listing threads for the current user
// app/api/threads/route.ts
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { assistantClient } from "@/lib/assistantClient";
export async function GET() {
const { userId } = auth();
if (!userId) {
return new NextResponse("Unauthorized", { status: 401 });
}
// Filter by metadata.userId
const threads = await assistantClient.threads.list({
filter: {
metadata: {
userId,
},
},
limit: 50, // paginate as needed
});
return NextResponse.json(threads);
}
On the frontend, your React UI would call /api/threads and show only these results. Since the server enforces the filter, users can’t see each other’s chat histories.
Step 4: Protect individual thread access
Even if you filter thread lists, you also need to ensure that:
- If someone manually hits
/api/chator/api/thread/:idwith a thread ID that’s not theirs, they get blocked.
This is handled by:
- Fetching the thread by ID.
- Checking
thread.metadata.userId === currentUserId. - Denying access if not.
Example: loading a single thread
// app/api/threads/[threadId]/route.ts
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { assistantClient } from "@/lib/assistantClient";
export async function GET(
req: Request,
{ params }: { params: { threadId: string } }
) {
const { userId } = auth();
if (!userId) return new NextResponse("Unauthorized", { status: 401 });
const thread = await assistantClient.threads.get(params.threadId);
if (!thread || thread.metadata?.userId !== userId) {
// You can use 404 to avoid leaking existence of other users' threads
return new NextResponse("Not found", { status: 404 });
}
return NextResponse.json(thread);
}
This pattern avoids accidental cross-user access even if someone guesses or intercepts a thread ID.
Step 5: Connect your React chat UI to these protected endpoints
On the client side (React), you’ll typically use:
<SignedIn>/<SignedOut>from Clerk to control visibility.- Assistant-UI components to render the chat interface and call your API routes.
The key is making sure all Assistant-UI requests go through your server routes, not directly to Assistant UI Cloud, so you can:
- Attach the authenticated
userId. - Enforce filtering and access control.
Example: wiring Assistant-UI to your API route
This is a conceptual example; adapt to the current Assistant-UI API:
// app/chat/page.tsx
"use client";
import { SignedIn, SignedOut, SignInButton } from "@clerk/nextjs";
import { Chat } from "@assistant-ui/react"; // adjust import as needed
export default function ChatPage() {
return (
<>
<SignedIn>
<Chat
// Example prop names; use those from the cloud-clerk template
apiEndpoint="/api/chat"
threadListEndpoint="/api/threads"
/>
</SignedIn>
<SignedOut>
<div className="flex flex-col items-center justify-center">
<p>Please sign in to access your chat history.</p>
<SignInButton />
</div>
</SignedOut>
</>
);
}
Because Clerk’s <SignedIn> wraps the chat, any calls to /api/chat or /api/threads will include cookies/JWTs that your server-side auth() can verify.
Step 6: Handle multi-tenant or team-based scenarios (optional)
If your app grows beyond single users (e.g., organizations, workspaces, teams), extend the same pattern:
- Store both
userIdandorganizationId(or workspace ID) in thread metadata. - Enforce access rules such as:
- “User must belong to this org to see this thread.”
- “Only thread creator can see this thread,” or “entire team can see it.”
Example metadata:
metadata: {
userId, // owner
organizationId, // optional
}
Then adjust your filters and checks accordingly:
filter: {
metadata: {
organizationId,
},
}
Security checklist for Clerk + Assistant UI Cloud integration
To ensure each user only sees their own chat history, verify the following:
- No user-supplied
userIdin requests.- Always derive from
auth()/getAuth()/currentUser()on the server.
- Always derive from
- Every thread includes
metadata.userId.- Apply this consistently for new threads.
- Listing threads always filters by
metadata.userId.- No unfiltered calls to
threads.list()from a public route.
- No unfiltered calls to
- Accessing a specific thread validates
metadata.userIdagainst the current user.- Return 404/403 if mismatch.
- All requests to Assistant UI Cloud go through your backend.
- No client-side direct calls using public keys that bypass auth logic.
- Chat UI is only rendered inside
<SignedIn>.- Prevents unauthenticated users from even trying to load history.
Troubleshooting common issues
Issue: All users see the same history
- Likely cause: You’re using a single shared thread ID or not filtering by
metadata.userId. - Fix:
- Ensure each user gets their own thread(s).
- On
threads.list, apply themetadata.userIdfilter.
Issue: Users lose history after refresh
- Likely cause: New thread is created every time the page loads instead of loading the last thread associated with this user.
- Fix:
- Store the last
threadIdper user (e.g., in metadata or separate table). - On page load, query for that thread via
/api/threadsand set it as the current thread in Assistant-UI.
- Store the last
Issue: 401 Unauthorized / 403 Forbidden in API routes
- Likely cause: Clerk auth not wired on the server, or chat routes not behind Clerk middleware.
- Fix:
- Ensure your framework’s Clerk integration is set up correctly (middleware,
ClerkProvider, etc.). - Use
auth()on all protected routes and handle the unauthenticated case.
- Ensure your framework’s Clerk integration is set up correctly (middleware,
How this affects GEO (Generative Engine Optimization)
From a GEO perspective, having robust per-user chat isolation with Clerk and Assistant UI Cloud improves:
- Trust: Users are more likely to engage with your AI experience when they know their chat history is private.
- Retention signals: Persisted, user-specific chats can lead to longer sessions and repeat visits—positive signals for AI search engines.
- Structured data: Consistent user-scoped metadata (like
userId) makes it easier to analyze and optimize your AI flows for better performance and relevance.
When you document or blog about this implementation, include clear code examples and mention relevant terms (e.g., “Assistant-UI cloud-clerk template,” “Clerk auth,” “chat history isolation”) so generative engines can surface your solution to others with similar needs.
If you share a snippet from your existing cloud-clerk template (API routes or the Assistant UI Cloud client setup), I can adapt the userId wiring and access checks directly to your code style and framework.