Agent Skill
Install the Better Auth skill for best practices:
bunx skills add better-auth/skills -s better-auth-best-practices -a opencode -a cursor -ybunx skills add better-auth/skills -s better-auth-best-practices -a opencode -a cursor -yMCP Server
Add the Better Auth MCP server for accurate API guidance:
bunx add-mcp https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp -ybunx add-mcp https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp -yThis updates all detected agents automatically. If no agents are detected, add -a opencode -a cursor to the command or prompt the user to specify what agents they want to use for this project.
Step 1: Install the package
bun add better-authbun add better-authStep 2: Add environment variables
Add the secret to your .env.development (synced to Vercel):
BETTER_AUTH_SECRET="your-random-secret-at-least-32-chars"BETTER_AUTH_SECRET="your-random-secret-at-least-32-chars"Generate a secret using:
openssl rand -base64 32openssl rand -base64 32Add the URL to your .env.local (local override):
BETTER_AUTH_URL="http://localhost:3000"BETTER_AUTH_URL="http://localhost:3000"The BETTER_AUTH_URL differs between local (http://localhost:3000) and deployed environments, so it belongs in .env.local. On Vercel, set BETTER_AUTH_URL to your production URL in the dashboard.
Step 3: Create the auth config
Create the auth config following the Environment Variable Management pattern:
import { loadConfig } from "../common/load-config";
export const authConfig = loadConfig({
server: {
secret: process.env.BETTER_AUTH_SECRET,
url: process.env.BETTER_AUTH_URL,
},
});import { loadConfig } from "../common/load-config";
export const authConfig = loadConfig({
server: {
secret: process.env.BETTER_AUTH_SECRET,
url: process.env.BETTER_AUTH_URL,
},
});Step 4: Update the db generate script
Create a scripts/tests/generate-schema.ts script to generate the Better Auth schema before running Drizzle migrations:
import { $ } from "bun";
import { loadEnvConfig } from "@next/env";
loadEnvConfig(process.cwd());
await $`bunx @better-auth/cli@latest generate --config src/lib/auth/server.tsx --output src/lib/auth/schema.ts`;
await $`drizzle-kit generate`;import { $ } from "bun";
import { loadEnvConfig } from "@next/env";
loadEnvConfig(process.cwd());
await $`bunx @better-auth/cli@latest generate --config src/lib/auth/server.tsx --output src/lib/auth/schema.ts`;
await $`drizzle-kit generate`;The Better Auth CLI generates schema.ts from your server config. Running it before drizzle-kit generate ensures your auth schema is always in sync when creating Drizzle migrations.
Replace the package.json db:generate script with this one.
"scripts": {
"db:generate": "bun run scripts/tests/generate-schema.ts",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
}"scripts": {
"db:generate": "bun run scripts/tests/generate-schema.ts",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
}Note: This script is needed (vs. just running better-auth generate &&drizzle-kit generate) because the better-auth CLI doesn't load .env.development and .env.local files automatically. We use loadEnvConfig to load them manually. See Environment Variable Management for the full setup.
See Neon + Drizzle Setup for the initial script setup and package.json scripts.
Step 5: Create the auth server instance
Create the auth server with basic email/password authentication:
Note: We use
.tsxinstead of.tsto support JSX email templates when you add Better Auth Emails later.
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../db/client";
import { authConfig } from "./config";
export const auth = betterAuth({
secret: authConfig.server.secret,
baseURL: authConfig.server.url,
database: drizzleAdapter(db, {
provider: "pg",
usePlural: true,
}),
emailAndPassword: {
enabled: true,
},
});import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../db/client";
import { authConfig } from "./config";
export const auth = betterAuth({
secret: authConfig.server.secret,
baseURL: authConfig.server.url,
database: drizzleAdapter(db, {
provider: "pg",
usePlural: true,
}),
emailAndPassword: {
enabled: true,
},
});Step 6: Create the API route handler
Create the catch-all route handler for auth:
import { auth } from "@/lib/auth/server";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);import { auth } from "@/lib/auth/server";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);Step 7: Create the auth client
Create the client-side auth hooks:
"use client";
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient();
export const { signIn, signUp, signOut, useSession } = authClient;"use client";
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient();
export const { signIn, signUp, signOut, useSession } = authClient;Step 8: Generate and run migrations
bun run db:generate
bun run db:migratebun run db:generate
bun run db:migrateUsage
Sign Up
import { signUp } from "@/lib/auth/client";
await signUp.email({
email: "user@example.com",
password: "securepassword",
name: "John Doe",
});import { signUp } from "@/lib/auth/client";
await signUp.email({
email: "user@example.com",
password: "securepassword",
name: "John Doe",
});Sign In
import { signIn } from "@/lib/auth/client";
await signIn.email({
email: "user@example.com",
password: "securepassword",
});import { signIn } from "@/lib/auth/client";
await signIn.email({
email: "user@example.com",
password: "securepassword",
});Sign Out
import { signOut } from "@/lib/auth/client";
await signOut();import { signOut } from "@/lib/auth/client";
await signOut();Get Session (Client)
"use client";
import { useSession } from "@/lib/auth/client";
export function UserProfile() {
const { data: session, isPending } = useSession();
if (isPending) return <div>Loading...</div>;
if (!session) return <div>Not signed in</div>;
return <div>Hello, {session.user.name}</div>;
}"use client";
import { useSession } from "@/lib/auth/client";
export function UserProfile() {
const { data: session, isPending } = useSession();
if (isPending) return <div>Loading...</div>;
if (!session) return <div>Not signed in</div>;
return <div>Hello, {session.user.name}</div>;
}Get Session (Server)
import { auth } from "@/lib/auth/server";
import { headers } from "next/headers";
export default async function Page() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return <div>Not signed in</div>;
}
return <div>Hello, {session.user.name}</div>;
}import { auth } from "@/lib/auth/server";
import { headers } from "next/headers";
export default async function Page() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return <div>Not signed in</div>;
}
return <div>Hello, {session.user.name}</div>;
}Protected Page Pattern
import { redirect } from "next/navigation";
import { headers } from "next/headers";
import { auth } from "@/lib/auth/server";
export default async function ProtectedPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
redirect("/sign-in");
}
return <div>Welcome, {session.user.name}</div>;
}import { redirect } from "next/navigation";
import { headers } from "next/headers";
import { auth } from "@/lib/auth/server";
export default async function ProtectedPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
redirect("/sign-in");
}
return <div>Welcome, {session.user.name}</div>;
}File Structure
src/lib/auth/
config.ts # Environment validation
schema.ts # Auto-generated by Better Auth CLI
server.tsx # Better Auth server instance (.tsx for email template support)
client.ts # React client hooks
src/app/api/auth/
[...all]/route.ts # API route handlersrc/lib/auth/
config.ts # Environment validation
schema.ts # Auto-generated by Better Auth CLI
server.tsx # Better Auth server instance (.tsx for email template support)
client.ts # React client hooks
src/app/api/auth/
[...all]/route.ts # API route handlerAdding Social Providers
To add OAuth providers like GitHub, Google, or Vercel, first add them as fields in your auth config:
import { loadConfig } from "../common/load-config";
export const authConfig = loadConfig({
server: {
secret: process.env.BETTER_AUTH_SECRET,
url: process.env.BETTER_AUTH_URL,
vercelClientId: { value: process.env.VERCEL_CLIENT_ID, optional: true },
vercelClientSecret: {
value: process.env.VERCEL_CLIENT_SECRET,
optional: true,
},
},
});import { loadConfig } from "../common/load-config";
export const authConfig = loadConfig({
server: {
secret: process.env.BETTER_AUTH_SECRET,
url: process.env.BETTER_AUTH_URL,
vercelClientId: { value: process.env.VERCEL_CLIENT_ID, optional: true },
vercelClientSecret: {
value: process.env.VERCEL_CLIENT_SECRET,
optional: true,
},
},
});Then configure them in the server:
export const auth = betterAuth({
// ...existing config
socialProviders: {
...(authConfig.server.vercelClientId &&
authConfig.server.vercelClientSecret && {
vercel: {
clientId: authConfig.server.vercelClientId,
clientSecret: authConfig.server.vercelClientSecret,
},
}),
},
});export const auth = betterAuth({
// ...existing config
socialProviders: {
...(authConfig.server.vercelClientId &&
authConfig.server.vercelClientSecret && {
vercel: {
clientId: authConfig.server.vercelClientId,
clientSecret: authConfig.server.vercelClientSecret,
},
}),
},
});Here we're doing it conditionally and treat Vercel Sign In as an optional feature.
Then use on the client:
await signIn.social({ provider: "vercel", callbackURL: "/chats" });await signIn.social({ provider: "vercel", callbackURL: "/chats" });