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

> For AI agents: a documentation index is available at [llms.txt](/llms.txt). Use `.md` for canonical markdown pages; `.mdx` is kept as a backwards-compatible alias on supported URL paths.

[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

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

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 }`.

1. ### 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](https://authjs.dev/guides/extending-the-session) for the full pattern.

2. ### 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-5.4-nano"),
       messages: await convertToModelMessages(messages),
     });
     return result.toUIMessageStreamResponse();
   }
   ```

3. ### 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.

   ```
   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.

4. ### 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.

5. ### 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

- **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

- [Custom thread persistence](/docs/integrations/persistence/custom-adapter) — The persistence story this page assumes; required for per-user threads outside AssistantCloud.
- [AssistantCloud](/docs/cloud) — The managed alternative; cloud handles JWT exchange with Clerk, Auth0, Supabase, Firebase.
- [Threads (concepts)](/docs/runtimes/concepts/threads) — The full RemoteThreadListAdapter contract and reload-on-auth pattern.