{"content":"# Better Auth Setup\n\nAdd user authentication using Better Auth with Drizzle ORM and Neon Postgres. Base setup with email/password authentication.\n\n### Agent Skill\n\nInstall the Better Auth skill for best practices:\n\n```bash\nbunx skills add better-auth/skills -s better-auth-best-practices -a opencode -a cursor -y\n```\n\n---\n\n### MCP Server\n\nAdd the Better Auth MCP server for accurate API guidance:\n\n```bash\nbunx add-mcp https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp -y\n```\n\nThis 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.\n\n---\n\n### Step 1: Install the package\n\n```bash\nbun add better-auth\n```\n\n### Step 2: Add environment variables\n\nAdd the secret to your `.env.development` (synced to Vercel):\n\n```env\nBETTER_AUTH_SECRET=\"your-random-secret-at-least-32-chars\"\n```\n\nGenerate a secret using:\n\n```bash\nopenssl rand -base64 32\n```\n\nAdd the URL to your `.env.local` (local override):\n\n```env\nBETTER_AUTH_URL=\"http://localhost:3000\"\n```\n\nThe `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.\n\n### Step 3: Create the auth config\n\nCreate the auth config following the [Environment Variable Management](/recipes/env-management) pattern:\n\n```typescript\n// src/lib/auth/config.ts\nimport { configSchema, server } from \"better-env/config-schema\";\n\nexport const authConfig = configSchema(\"Auth\", {\n  secret: server({ env: \"BETTER_AUTH_SECRET\" }),\n  url: server({ env: \"BETTER_AUTH_URL\" }),\n});\n```\n\n### Step 4: Update the db generate script\n\nCreate a `scripts/tests/generate-schema.ts` script to generate the Better Auth schema before running Drizzle migrations:\n\n```typescript\n// scripts/tests/generate-schema.ts\nimport { $ } from \"bun\";\nimport { loadEnvConfig } from \"@next/env\";\n\nloadEnvConfig(process.cwd());\n\nawait $`bunx @better-auth/cli@latest generate --config src/lib/auth/server.tsx --output src/lib/auth/schema.ts`;\n\nawait $`drizzle-kit generate`;\n```\n\nThe 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.\n\nReplace the `package.json` `db:generate` script with this one.\n\n```json\n\"scripts\": {\n  \"db:generate\": \"bun run scripts/tests/generate-schema.ts\",\n  \"db:migrate\": \"drizzle-kit migrate\",\n  \"db:studio\": \"drizzle-kit studio\"\n}\n```\n\nNote: 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](/recipes/env-management) for the full setup.\n\nSee [Neon + Drizzle Setup](/recipes/drizzle-with-node-postgres) for the initial script setup and `package.json` scripts.\n\n### Step 5: Create the auth server instance\n\nCreate the auth server with basic email/password authentication:\n\n> **Note:** We use `.tsx` instead of `.ts` to support JSX email templates when you add [Better Auth Emails](/recipes/better-auth-emails) later.\n\n```tsx\n// src/lib/auth/server.tsx\nimport { betterAuth } from \"better-auth\";\nimport { drizzleAdapter } from \"better-auth/adapters/drizzle\";\nimport { db } from \"../db/client\";\nimport { authConfig } from \"./config\";\n\nexport const auth = betterAuth({\n  secret: authConfig.server.secret,\n  baseURL: authConfig.server.url,\n  database: drizzleAdapter(db, {\n    provider: \"pg\",\n    usePlural: true,\n  }),\n  emailAndPassword: {\n    enabled: true,\n  },\n});\n```\n\n### Step 6: Create the API route handler\n\nCreate the catch-all route handler for auth:\n\n```typescript\n// src/app/api/auth/[...all]/route.ts\nimport { auth } from \"@/lib/auth/server\";\nimport { toNextJsHandler } from \"better-auth/next-js\";\n\nexport const { POST, GET } = toNextJsHandler(auth);\n```\n\n### Step 7: Create the auth client\n\nCreate the client-side auth hooks:\n\n```typescript\n// src/lib/auth/client.ts\n\"use client\";\n\nimport { createAuthClient } from \"better-auth/react\";\n\nexport const authClient = createAuthClient();\n\nexport const { signIn, signUp, signOut, useSession } = authClient;\n```\n\n### Step 8: Generate and run migrations\n\n```bash\nbun run db:generate\nbun run db:migrate\n```\n\n---\n\n## Usage\n\n### Sign Up\n\n```typescript\nimport { signUp } from \"@/lib/auth/client\";\n\nawait signUp.email({\n  email: \"user@example.com\",\n  password: \"securepassword\",\n  name: \"John Doe\",\n});\n```\n\n### Sign In\n\n```typescript\nimport { signIn } from \"@/lib/auth/client\";\n\nawait signIn.email({\n  email: \"user@example.com\",\n  password: \"securepassword\",\n});\n```\n\n### Sign Out\n\n```typescript\nimport { signOut } from \"@/lib/auth/client\";\n\nawait signOut();\n```\n\n### Get Session (Client)\n\n```tsx\n\"use client\";\n\nimport { useSession } from \"@/lib/auth/client\";\n\nexport function UserProfile() {\n  const { data: session, isPending } = useSession();\n\n  if (isPending) return <div>Loading...</div>;\n  if (!session) return <div>Not signed in</div>;\n\n  return <div>Hello, {session.user.name}</div>;\n}\n```\n\n### Get Session (Server)\n\n```typescript\nimport { auth } from \"@/lib/auth/server\";\nimport { headers } from \"next/headers\";\n\nexport default async function Page() {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  if (!session) {\n    return <div>Not signed in</div>;\n  }\n\n  return <div>Hello, {session.user.name}</div>;\n}\n```\n\n### Protected Page Pattern\n\n```tsx\nimport { redirect } from \"next/navigation\";\nimport { headers } from \"next/headers\";\nimport { auth } from \"@/lib/auth/server\";\n\nexport default async function ProtectedPage() {\n  const session = await auth.api.getSession({\n    headers: await headers(),\n  });\n\n  if (!session) {\n    redirect(\"/sign-in\");\n  }\n\n  return <div>Welcome, {session.user.name}</div>;\n}\n```\n\n---\n\n## File Structure\n\n```\nsrc/lib/auth/\n  config.ts    # Environment validation\n  schema.ts    # Auto-generated by Better Auth CLI\n  server.tsx   # Better Auth server instance (.tsx for email template support)\n  client.ts    # React client hooks\n\nsrc/app/api/auth/\n  [...all]/route.ts  # API route handler\n```\n\n---\n\n## Adding Social Providers\n\nTo add OAuth providers like GitHub, Google, or Vercel, first add them as fields in your auth config:\n\n```typescript\n// src/lib/auth/config.ts\nimport { configSchema, server } from \"better-env/config-schema\";\n\nexport const authConfig = configSchema(\"Auth\", {\n  secret: server({ env: \"BETTER_AUTH_SECRET\" }),\n  url: server({ env: \"BETTER_AUTH_URL\" }),\n  vercelClientId: server({ env: \"VERCEL_CLIENT_ID\", optional: true }),\n  vercelClientSecret: server({ env: \"VERCEL_CLIENT_SECRET\", optional: true }),\n});\n```\n\nThen configure them in the server:\n\n```tsx\n// src/lib/auth/server.tsx\nexport const auth = betterAuth({\n  // ...existing config\n  socialProviders: {\n    ...(authConfig.server.vercelClientId &&\n      authConfig.server.vercelClientSecret && {\n        vercel: {\n          clientId: authConfig.server.vercelClientId,\n          clientSecret: authConfig.server.vercelClientSecret,\n        },\n      }),\n  },\n});\n```\n\nHere we're doing it conditionally and treat Vercel Sign In as an optional feature.\n\nThen use on the client:\n\n```typescript\nawait signIn.social({ provider: \"vercel\", callbackURL: \"/chats\" });\n```\n\n---\n\n## References\n\n- [Better Auth Next.js Docs](https://www.better-auth.com/docs/integrations/next)\n- [Better Auth Drizzle Adapter](https://www.better-auth.com/docs/adapters/drizzle)"}