Gate the chat route and scope thread persistence to the signed-in user with Auth.js v5.
Auth.js (formerly NextAuth) is the dominant OSS auth library in the Next.js ecosystem. This page covers using it with assistant-ui without AssistantCloud: session-cookie auth, server-side auth() checks on the API route, and per-user scoping on a custom thread list.
If you use AssistantCloud, see cloud authorization instead; cloud handles JWT exchange for you.
How it works
browser ──► /api/chat ──► auth() returns session
/api/threads/* │
▼
scope queries by session.user.id
│
▼
databaseThree places auth touches the integration:
- API routes (
/api/chat,/api/threads/*) callauth()server-side, return 401 if no session, then scope DB queries bysession.user.id. RemoteThreadListAdapterdoes nothing auth-specific; the cookie travels with thefetchcalls automatically (same origin).- Reload-on-auth: if the session resolves after the initial render, call
aui.threads().reload()so the list re-fetches with the new identity.
Setup
This guide assumes you already have Auth.js configured. If not, follow the Auth.js installation guide first; the steps below pick up after auth.ts exports { handlers, signIn, signOut, auth }.
Populate session.user.id
By default, Auth.js v5 does not include a stable id on session.user; only email, name, and image are exposed. Without this step, the scoping queries below all run with userId: undefined and silently match every row.
Add jwt and session callbacks to your Auth.js config so the user id flows through. For the JWT session strategy:
import NextAuth from "next-auth";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [/* your providers */],
callbacks: {
async jwt({ token, user }) {
if (user) token.id = user.id;
return token;
},
async session({ session, token }) {
if (token.id) session.user.id = token.id as string;
return session;
},
},
});If you use the database session strategy, replace the jwt/session pair with a single session callback that copies from the user argument: session.user.id = user.id. See Auth.js: extending the session for the full pattern.
Gate the chat route
Add an auth() check to the existing AI SDK route. Return 401 before calling the model so unauthenticated traffic doesn't burn provider credits.
import { auth } from "@/auth";
import { openai } from "@ai-sdk/openai";
import { streamText, convertToModelMessages } from "ai";
import type { UIMessage } from "ai";
export async function POST(req: Request) {
const session = await auth();
if (!session?.user) return new Response("Unauthorized", { status: 401 });
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}Scope thread queries by user
If you also use custom thread persistence, every thread-list endpoint must filter by session.user.id. Without scoping, any signed-in user can list everyone's threads.
import { auth } from "@/auth";
import { db } from "@/db";
import { threads } from "@/db/schema";
import { eq, desc } from "drizzle-orm";
export async function GET() {
const session = await auth();
if (!session?.user) return new Response(null, { status: 401 });
const rows = await db
.select()
.from(threads)
.where(eq(threads.userId, session.user.id))
.orderBy(desc(threads.updatedAt));
return Response.json(rows);
}Apply the same pattern to POST, PATCH, and DELETE handlers. Always verify ownership before mutating.
Reload threads after async auth
The first render of <MyProvider> may run before auth() resolves on the client. The thread list will be empty until the user refreshes. Drop a small effect into the layout to call aui.threads().reload() once the session resolves.
useSession requires <SessionProvider> higher in the tree. Wrap your root layout's client subtree once:
"use client";
import { SessionProvider } from "next-auth/react";
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}Then inside the assistant runtime provider, mount a small effect:
"use client";
import { useAui } from "@assistant-ui/react";
import { useSession } from "next-auth/react";
import { useEffect } from "react";
export function ReloadOnAuth() {
const aui = useAui();
const { status, data } = useSession();
useEffect(() => {
if (status === "authenticated") aui.threads().reload();
}, [status, data?.user?.id]);
return null;
}reload() discards in-flight responses from superseded calls, so it is safe to invoke on every auth transition.
Run and verify
Sign in. Check:
/api/chatreturns200with a streaming response when authenticated;401when not./api/threadsreturns only the current user's threads.- A second user (incognito tab, different account) sees a different thread list.
- Signing out and back in does not show the previous user's threads even briefly; if it does,
ReloadOnAuthisn't mounted or isn't watching the right session field.
Notes
- Same-origin cookies. Browser cookies travel automatically when frontend and API are on the same origin. If you split the API onto a different host, set
credentials: "include"on everyfetchand configure CORS on the API. - Edge runtime.
auth()runs in Edge contexts only when your DB adapter is edge-compatible. If you use Prisma, Drizzle-over-pg, or another non-edge adapter together with Edge route handlers or pre-Next.js 16 middleware, follow Auth.js's split-config pattern: keep a sharedauth.config.tswithout the adapter, instantiate the fullauth.tswith the adapter elsewhere, and import only the config-instantiatedauthinside middleware. Next.js 16+ runsproxy.tson Node, so this workaround is generally unnecessary there. - Roles and orgs. For B2B SaaS where admins see all threads under an org, add
orgIdto thethreadstable, store the user's role in the session via the Auth.js callbacks, and gatelist()accordingly. The pattern is the same; thewhereclause just gets richer. - OAuth providers. This page is provider-agnostic. The same
auth()pattern works whether you wired GitHub, Google, credentials, or a passkey provider inauth.ts.