# 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]