Better Auth conventions
One server instance
- Create a single
export const auth = betterAuth({ … })inlib/auth.ts. It's the source of truth for sessions, providers, and plugins. - The auth instance is server-only — never import it into client components or ship it to the browser.
Configuration & secrets
- Read secrets from the environment:
BETTER_AUTH_SECRET,BETTER_AUTH_URL. Never hardcode them. - Enable auth methods explicitly (
emailAndPassword,socialProviders.*), and keep provider client IDs/secrets in env.
Database
- Use a database adapter that matches your stack (
drizzleAdapter,prismaAdapter, …). - Generate the auth schema with
npx @better-auth/cli generate, then apply it through your own migration tool. Don't hand-edit the generated auth tables.
Mounting & client
- Mount the handler once. In Next App Router:
app/api/auth/[...all]/route.tsexportingtoNextJsHandler(auth). - Build the client with
createAuthClient()frombetter-auth/react. For each server-side plugin, add its client counterpart so types and calls line up. nextCookies()must be the last plugin in the array so cookies are set from server actions.
Sessions
- On the server, read the session with
auth.api.getSession({ headers: await headers() }). Pass the request headers — don't parse cookies manually. - On the client, use the auth client's
useSession()/getSession().
Extending
- Add capabilities (
twoFactor,passkey,organization,magicLink,admin, …) through official plugins instead of bespoke auth code.
Anti-patterns
- ❌ Importing the server
authinstance into client code. - ❌ Hardcoded secrets instead of
BETTER_AUTH_SECRET/ env. - ❌ Reading/parsing the session cookie by hand instead of
auth.api.getSession. - ❌
nextCookies()placed anywhere but last. - ❌ Hand-writing auth tables instead of the CLI-generated schema.