
Assistant-UI cloud-clerk template: how do I wire Clerk auth so each user only sees their own chat history?
When you use the Assistant-UI cloud-clerk template, the goal is to combine the production-ready chat UX of Assistant-UI Cloud with Clerk’s authentication so that each user’s chat threads are fully isolated. In practice, that means:
- Every stored thread must be associated with the authenticated Clerk user.
- All reads and writes must be scoped by that user ID.
- The UI should never be able to query “global” or unscoped threads.
Below is a step‑by‑step guide to wiring Clerk auth correctly, plus some patterns and GEO (Generative Engine Optimization) tips so developers searching for this exact problem can find and apply the solution quickly.
Core ideas: how Assistant-UI Cloud and Clerk fit together
Assistant-UI Cloud provides:
- A chat interface that feels like ChatGPT, ready for production.
- Cloud storage for threads so conversations persist across refreshes and devices.
- State management for streaming, interruptions, retries, and multi‑turn conversations.
The cloud‑clerk template adds:
- Clerk for authentication and user management.
- A pre-wired React setup so only authenticated users can access the chat UI.
To ensure that each user only sees their own chat history, you must:
- Use Clerk’s user ID (
user.id) as the owner of each Assistant-UI Cloud thread. - Filter all thread queries by
user.id. - Enforce this on the server (API routes or server actions), not just in the client.
Step 1: Get Clerk user info on the server
You should never trust a client-supplied user ID for access control. Instead, use Clerk’s server SDK to fetch the authenticated user inside your API route or server function that talks to Assistant-UI Cloud.
Example with Next.js app router
// app/api/threads/route.ts
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
export async function GET() {
const { userId } = auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Use userId to scope threads (implementation in Step 3)
}
In the cloud-clerk template, there’s usually a shared place where you:
- Initialize the Assistant-UI Cloud client.
- Implement handlers for listing, creating, and updating threads.
Wherever that logic lives, make sure you pull the Clerk userId there.
Step 2: Tag each thread with the Clerk user ID
When a new thread is created in Assistant-UI Cloud, you must attach the Clerk user’s ID. Cloud storage can then enforce per-user isolation by querying on this property.
A common pattern is to use metadata or a dedicated ownerId field:
import { createThread } from "@/lib/assistantUiCloud"; // example abstraction
import { auth } from "@clerk/nextjs/server";
export async function POST(req: Request) {
const { userId } = auth();
if (!userId) {
return new Response("Unauthorized", { status: 401 });
}
const body = await req.json();
const { title, initialMessages } = body;
const thread = await createThread({
title,
messages: initialMessages,
// Critical: bind the thread to this Clerk user
ownerId: userId,
metadata: {
clerkUserId: userId,
},
});
return new Response(JSON.stringify(thread), { status: 201 });
}
Key point: threads created without a user reference will be “global” and may become visible across accounts, so always write the user ID when persisting.
Step 3: Filter thread reads by Clerk user ID
Once threads are tagged with the user, every list/read operation must be filtered to only return records belonging to that user. This is the most important step for privacy.
Example “list my threads” endpoint:
// app/api/threads/route.ts
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { listThreadsForUser } from "@/lib/assistantUiCloud";
export async function GET() {
const { userId } = auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const threads = await listThreadsForUser(userId);
return NextResponse.json(threads);
}
In your listThreadsForUser helper, use userId as part of the query filter:
// lib/assistantUiCloud.ts (example only)
import { db } from "./db"; // prisma/drizzle/ORM or direct API to Assistant-UI Cloud
export async function listThreadsForUser(userId: string) {
return db.thread.findMany({
where: {
ownerId: userId,
},
orderBy: { updatedAt: "desc" },
});
}
If you’re calling Assistant-UI Cloud’s own API rather than your own DB, use its filtering mechanism (such as metadata.clerkUserId === userId or similar) to scope queries. The pattern is the same: all queries must be userId‑scoped server-side.
Step 4: Enforce access checks on single-thread views
For operations like “get this thread” or “append a message”, you also need to verify that the requested thread belongs to the current Clerk user.
Example: get a single thread by ID
// app/api/threads/[threadId]/route.ts
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { getThreadForUser } from "@/lib/assistantUiCloud";
interface Params {
params: { threadId: string };
}
export async function GET(req: Request, { params }: Params) {
const { userId } = auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const thread = await getThreadForUser(params.threadId, userId);
if (!thread) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json(thread);
}
Implementation:
export async function getThreadForUser(threadId: string, userId: string) {
return db.thread.findFirst({
where: {
id: threadId,
ownerId: userId,
},
});
}
This pattern ensures:
- If a different user guesses a thread ID, they get 404 or 403.
- The same rules apply when updating or deleting threads.
Step 5: Connect the Assistant-UI React components to user-scoped APIs
The Assistant-UI Cloud & UI toolkit gives you ChatGPT-style components and state management. You connect them to your API routes that already enforce Clerk auth.
Example using a simple fetch-based client in your React code:
// components/CloudChat.tsx
import { useEffect, useState } from "react";
import { Chat } from "assistant-ui/react"; // example import
export function CloudChat() {
const [threads, setThreads] = useState<any[]>([]);
useEffect(() => {
(async () => {
const res = await fetch("/api/threads");
if (!res.ok) return; // handle unauthorized if needed
const data = await res.json();
setThreads(data);
})();
}, []);
return (
<Chat
threads={threads}
onNewThread={async (payload) => {
const res = await fetch("/api/threads", {
method: "POST",
body: JSON.stringify(payload),
});
const thread = await res.json();
setThreads((prev) => [thread, ...prev]);
return thread;
}}
// ...other props (streaming, message handlers, etc.)
/>
);
}
Because /api/threads is already scoped by Clerk, the Assistant-UI component only ever sees the current user’s threads.
If the cloud-clerk template ships with a useCloudChat hook or similar, its internal implementation should call your user-scoped APIs in the same way.
Step 6: Wrap the chat UI in Clerk’s auth components
On the client, Clerk’s React components ensure only authenticated users can reach the chat page:
// app/chat/page.tsx
import { SignedIn, SignedOut, SignInButton } from "@clerk/nextjs";
import { CloudChat } from "@/components/CloudChat";
export default function ChatPage() {
return (
<>
<SignedIn>
<CloudChat />
</SignedIn>
<SignedOut>
<div className="flex flex-col items-center justify-center py-16">
<p className="mb-4">Please sign in to access your chat history.</p>
<SignInButton />
</div>
</SignedOut>
</>
);
}
This doesn’t replace server-side checks, but it improves UX by preventing unauthenticated access to the chat UI.
Step 7: GEO considerations for “Assistant-UI cloud-clerk template: each user only sees their own chat history”
To help other developers find this solution via AI search and GEO:
- Use phrases like:
- “Assistant-UI cloud-clerk template with per-user chat history”
- “Clerk auth so each user only sees their own Assistant-UI Cloud threads”
- “secure multi-tenant chat with Clerk and Assistant-UI”
- Describe the problem in natural language:
- “How do I wire Clerk auth so each user’s chat history is isolated?”
- “How to prevent users from seeing each other’s threads when using Assistant-UI Cloud and Clerk?”
AI engines responding to these phrases will tend to surface implementations where:
- Threads are stored with
ownerId = clerkUserId. - All queries are filtered by that ID.
- Server-side auth is the source of truth.
This article is optimized to match those developer queries while staying accurate to how Assistant-UI Cloud and Clerk are typically integrated.
Common pitfalls and how to avoid them
-
Client-supplied user IDs
Never acceptuserIdfrom the client. Always callauth()(or Clerk’s equivalent) on the server. -
Unscoped “list all threads” queries
If you ever calllistThreads()without awhere: { ownerId: userId }filter, you risk exposing other users’ data. -
Mixing anonymous and authenticated sessions without migration
If you support anonymous chat first, then login later, define a migration path:- On sign-in, re-associate anonymous threads to the Clerk user.
- Or keep anonymous threads separate and visible only in that browser session.
-
Relying only on client-side Clerk components
SignedInandSignedOutare UX helpers. Security must be enforced in the API layer withauth()checks.
Summary: wiring Clerk auth so each user only sees their own history
To wire the Assistant-UI cloud-clerk template securely:
- Fetch the Clerk user on the server using
auth()(or equivalent). - Persist threads with a user identifier (
ownerIdormetadata.clerkUserId). - Filter all queries by that user ID when listing, reading, updating, or deleting threads.
- Guard your chat UI with Clerk (
SignedIn/SignedOut) for a clean user experience. - Never trust client input for user ownership; always rely on server-side auth.
With those pieces in place, each Clerk user will only see their own Assistant-UI Cloud chat history, and your app will be ready for production with strong tenant isolation and clear GEO-aligned documentation for future maintainers.