# better-auth
URL: /docs/integrations/auth/better-auth

TypeScript-first auth with database-owned sessions; gate the chat route and scope threads to the signed-in user.

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

[better-auth](https://www.better-auth.com/) is a TypeScript-first auth library that owns the session, the user table, and the cookie story end to end. It pairs naturally with assistant-ui's [custom thread persistence](/docs/integrations/persistence/custom-adapter): the same database holds users and threads, and `session.user.id` flows directly into your where clauses.

This page covers the non-cloud path. AssistantCloud users should see [cloud authorization](/docs/cloud/authorization) for the JWT-exchange pattern.

## How it works

Three integration points:

1. **Auth handlers** mounted at `/api/auth/[...all]/route.ts` via `toNextJsHandler(auth)`.
2. **API routes** read the session server-side with `auth.api.getSession({ headers: await headers() })`.
3. **Reload-on-auth** uses better-auth's React client to detect sign-in and trigger `aui.threads().reload()`.

`session.user.id` is exposed on the session object directly, so route handlers can scope queries against it.

## Setup

This guide assumes you already have better-auth configured. If not, follow the [better-auth Next.js guide](https://www.better-auth.com/docs/integrations/next) first; the steps below pick up after `auth.ts` exports a `betterAuth(...)` instance.

1. ### Confirm the auth handler is mounted

   better-auth ships its own catch-all handler. Make sure it's wired:

   ```
   import { auth } from "@/auth";
   import { toNextJsHandler } from "better-auth/next-js";

   export const { GET, POST } = toNextJsHandler(auth);
   ```

   Without this, sign-in, sign-out, and session refresh requests have nowhere to land.

2. ### Gate the chat route

   Resolve the session server-side with `auth.api.getSession`. It accepts request headers (which carry the session cookie) and returns `null` for unauthenticated requests.

   ```
   import { auth } from "@/auth";
   import { headers } from "next/headers";
   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.api.getSession({ headers: await headers() });
     if (!session) 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();
   }
   ```

   `headers()` from `next/headers` returns the active request's headers in App Router route handlers. Always `await` it.

3. ### Scope thread queries by user

   If you use [custom thread persistence](/docs/integrations/persistence/custom-adapter), every endpoint must filter by `session.user.id`. The pattern mirrors the chat route:

   ```
   import { auth } from "@/auth";
   import { headers } from "next/headers";
   import { db } from "@/db";
   import { threads } from "@/db/schema";
   import { eq, desc } from "drizzle-orm";

   export async function GET() {
     const session = await auth.api.getSession({ headers: await headers() });
     if (!session) 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);
   }
   ```

   If your better-auth schema lives in the same database as your threads table, you can also reference `users.id` as a foreign key for cascading deletes.

4. ### Set up the React client

   Create the client once and import it from a shared module so all hooks share state:

   ```
   import { createAuthClient } from "better-auth/react";

   export const authClient = createAuthClient();
   ```

5. ### Reload threads after async auth

   `useSession` from the React client tracks the live session. Drop a small effect inside `<AssistantRuntimeProvider>` that reloads the thread list when the user signs in:

   ```
   "use client";

   import { useAui } from "@assistant-ui/react";
   import { authClient } from "@/lib/auth-client";
   import { useEffect } from "react";

   export function ReloadOnAuth() {
     const aui = useAui();
     const { data: session, isPending } = authClient.useSession();
     useEffect(() => {
       if (!isPending && session) aui.threads().reload();
     }, [isPending, session?.user?.id]);
     return null;
   }
   ```

   Mount this component anywhere inside your `<AssistantRuntimeProvider>` subtree (typically next to the runtime's provider in `MyProvider`). `reload()` discards in-flight responses from superseded calls, so it is safe to invoke on every auth transition.

6. ### Run and verify

   Sign in. Check:

   - `/api/chat` returns `200` when authenticated; `401` when not.
   - `/api/threads` returns only the current user's threads.
   - A second user (incognito tab) sees a different thread list.
   - The session cookie set by `/api/auth/sign-in/*` is `HttpOnly` and travels with same-origin fetches.

## Notes

- **`session.user.id` is populated by default.** The `id` field comes from the user row better-auth manages, so route handlers can scope queries without any callback configuration.
- **Cookie-based, same-origin.** Browser fetches automatically include the session cookie. If you split the API onto a different host, set `credentials: "include"` and configure CORS on the API.
- **Database alignment.** If you use Drizzle and put both better-auth's user schema and your threads schema in the same database, foreign keys keep deletes consistent. better-auth's CLI generates the user schema; add your `threads` table referencing `user.id`.
- **Edge runtime.** `auth.api.getSession` works in both Node and Edge runtimes when better-auth is configured for an edge-compatible adapter.
- **Cloud users.** [AssistantCloud's auth-provider integrations](/docs/cloud/authorization) cover Clerk, Auth0, Supabase, and Firebase via JWT exchange. To pair Cloud with better-auth, use the [backend-server token approach](/docs/cloud/authorization#backend-server-approach): resolve `session.user.id` server-side, mint an Assistant Cloud token with that id as the `userId`, and return it from a token endpoint.

## Related

- [Custom thread persistence](/docs/integrations/persistence/custom-adapter) — The persistence story this page builds on; required for per-user threads outside AssistantCloud.
- [Auth.js (next-auth)](/docs/integrations/auth/next-auth) — The other common Next.js auth library. Different session shape; same wiring approach.
- [Threads (concepts)](/docs/runtimes/concepts/threads) — The full RemoteThreadListAdapter contract and reload-on-auth pattern.