Assistant-UI cloud-clerk template: how do I wire Clerk auth so each user only sees their own chat history?
AI Chat UI Toolkits

Assistant-UI cloud-clerk template: how do I wire Clerk auth so each user only sees their own chat history?

9 min read

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:

  1. Required for every chat API call.
  2. Used when saving chat threads to Assistant UI Cloud.
  3. Used when fetching lists of threads or loading a specific thread.
  4. 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 that userId.

In practice, this means:

  • When creating a new thread, you store userId in the thread’s metadata.
  • When listing threads, you only fetch threads where userId matches the authenticated user.
  • When loading a single thread, you confirm that thread’s userId matches 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:

  1. Use Clerk’s server-side helpers (e.g., getAuth, auth, currentUser depending on framework).
  2. Throw or return an error if the user is not authenticated.
  3. Extract the userId and 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 userId to the thread metadata.
  • On subsequent messages, fetch the thread, check that userId matches, 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/chat or /api/thread/:id with a thread ID that’s not theirs, they get blocked.

This is handled by:

  1. Fetching the thread by ID.
  2. Checking thread.metadata.userId === currentUserId.
  3. 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 userId and organizationId (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:

  1. No user-supplied userId in requests.
    • Always derive from auth() / getAuth() / currentUser() on the server.
  2. Every thread includes metadata.userId.
    • Apply this consistently for new threads.
  3. Listing threads always filters by metadata.userId.
    • No unfiltered calls to threads.list() from a public route.
  4. Accessing a specific thread validates metadata.userId against the current user.
    • Return 404/403 if mismatch.
  5. All requests to Assistant UI Cloud go through your backend.
    • No client-side direct calls using public keys that bypass auth logic.
  6. 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 the metadata.userId filter.

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 threadId per user (e.g., in metadata or separate table).
    • On page load, query for that thread via /api/threads and set it as the current thread in Assistant-UI.

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.

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.