# Auth.js (next-auth) URL: /docs/integrations/auth/next-auth Gate the chat route and scope thread persistence to the signed-in user with Auth.js v5. [Auth.js](https://authjs.dev/) (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](/docs/cloud) instead; cloud handles JWT exchange for you. ## How it works \[#how-it-works] ``` browser ──► /api/chat ──► auth() returns session /api/threads/* │ ▼ scope queries by session.user.id │ ▼ database ``` Three places auth touches the integration: 1. **API routes** (`/api/chat`, `/api/threads/*`) call `auth()` server-side, return 401 if no session, then scope DB queries by `session.user.id`. 2. **`RemoteThreadListAdapter`** does nothing auth-specific; the cookie travels with the `fetch` calls automatically (same origin). 3. **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 \[#setup] This guide assumes you already have Auth.js configured. If not, follow the [Auth.js installation guide](https://authjs.dev/getting-started/installation?framework=Next.js) first; the steps below pick up after `auth.ts` exports `{ handlers, signIn, signOut, auth }`. ### Populate `session.user.id` \[#populate-sessionuserid] 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: ```ts title="auth.ts" 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](https://authjs.dev/guides/extending-the-session) for the full pattern. ### Gate the chat route \[#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. ```ts title="app/api/chat/route.ts" 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 \[#scope-thread-queries-by-user] If you also use [custom thread persistence](/docs/integrations/persistence/custom-adapter), every thread-list endpoint must filter by `session.user.id`. Without scoping, any signed-in user can list everyone's threads. ```ts title="app/api/threads/route.ts" 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 \[#reload-threads-after-async-auth] The first render of `` 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 `` higher in the tree. Wrap your root layout's client subtree once: ```tsx title="app/providers.tsx" "use client"; import { SessionProvider } from "next-auth/react"; export function Providers({ children }: { children: React.ReactNode }) { return {children}; } ``` Then inside the assistant runtime provider, mount a small effect: ```tsx title="app/components/ReloadOnAuth.tsx" "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 \[#run-and-verify] Sign in. Check: * `/api/chat` returns `200` with a streaming response when authenticated; `401` when not. * `/api/threads` returns 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, `ReloadOnAuth` isn't mounted or isn't watching the right session field. ## Notes \[#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 every `fetch` and 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](https://authjs.dev/guides/edge-compatibility): keep a shared `auth.config.ts` without the adapter, instantiate the full `auth.ts` with the adapter elsewhere, and import only the config-instantiated `auth` inside middleware. Next.js 16+ runs `proxy.ts` on Node, so this workaround is generally unnecessary there. * **Roles and orgs.** For B2B SaaS where admins see all threads under an org, add `orgId` to the `threads` table, store the user's role in the session via the Auth.js callbacks, and gate `list()` accordingly. The pattern is the same; the `where` clause 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 in `auth.ts`. ## Related \[#related]