# 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. [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 \[#how-it-works] ``` browser ──► /api/auth/[...all] ──► better-auth handlers /api/chat │ /api/threads/* │ │ │ ▼ ▼ auth.api.getSession({ headers }) ──► session.user.id │ ▼ scope queries by userId ``` 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 \[#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. ### Confirm the auth handler is mounted \[#confirm-the-auth-handler-is-mounted] better-auth ships its own catch-all handler. Make sure it's wired: ```ts title="app/api/auth/[...all]/route.ts" 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. ### Gate the chat route \[#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. ```ts title="app/api/chat/route.ts" 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-4o"), 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. ### Scope thread queries by user \[#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: ```ts title="app/api/threads/route.ts" 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. ### Set up the React client \[#set-up-the-react-client] Create the client once and import it from a shared module so all hooks share state: ```ts title="lib/auth-client.ts" import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient(); ``` ### Reload threads after async auth \[#reload-threads-after-async-auth] `useSession` from the React client tracks the live session. Drop a small effect inside `` that reloads the thread list when the user signs in: ```tsx title="app/components/ReloadOnAuth.tsx" "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 `` 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. ### Run and verify \[#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 \[#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 \[#related]