Claude Code for Better Auth: Full-Stack Authentication Library — Claude Skills 360 Blog
Blog / Backend / Claude Code for Better Auth: Full-Stack Authentication Library
Backend

Claude Code for Better Auth: Full-Stack Authentication Library

Published: April 6, 2027
Read time: 8 min read
By: Claude Skills 360

Better Auth is a TypeScript-first authentication library with first-class Next.js and database ORM support — betterAuth({ database, emailAndPassword, socialProviders }) configures the server. createAuthClient() creates a typed client. authClient.signIn.email({ email, password }) logs in; authClient.signUp.email({ email, password, name }) registers. authClient.useSession() returns the current session as a React hook. OAuth is added with socialProvider({ providerId: "google", clientId, clientSecret }). The middleware plugin enforces route protection server-side. organization() plugin adds multi-tenancy with invite flows. Better Auth generates TypeScript types for your entire auth surface — routes, session, user, and plugins. It works with Prisma, Drizzle, and raw SQL adapters. Claude Code generates Better Auth server config, client setup, session hooks, OAuth providers, role-based middleware, and database adapter configuration.

CLAUDE.md for Better Auth

## Better Auth Stack
- Version: better-auth >= 1.1
- Server: betterAuth({ database, emailAndPassword: { enabled: true }, socialProviders: [...] })
- Client: const authClient = createAuthClient({ baseURL: "http://localhost:3000" })
- Sign in: await authClient.signIn.email({ email, password, callbackURL: "/dashboard" })
- Sign up: await authClient.signUp.email({ email, password, name })
- Session: const { data: session } = authClient.useSession() — React hook
- Next.js: export { GET, POST } = auth.handler — api/auth/[...all]/route.ts
- Middleware: auth.api.getSession() in next/server middleware
- Plugins: organization(), twoFactor(), passkey() — modular plugins

Server Configuration

// lib/auth.ts — Better Auth server setup
import { betterAuth } from "better-auth"
import { prismaAdapter } from "better-auth/adapters/prisma"
import { organization, twoFactor } from "better-auth/plugins"
import { db } from "@/lib/db"

export const auth = betterAuth({
  database: prismaAdapter(db, {
    provider: "postgresql",
  }),

  // Session configuration
  session: {
    expiresIn: 60 * 60 * 24 * 7,   // 7 days
    updateAge: 60 * 60 * 24,        // Refresh if older than 1 day
    cookieCache: {
      enabled: true,
      maxAge: 5 * 60,               // Cache session for 5 minutes client-side
    },
  },

  // Email/password auth
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
    minPasswordLength: 8,
    autoSignIn: true,  // Auto sign in after registration
  },

  // Email verification
  emailVerification: {
    sendVerificationEmail: async ({ user, url }) => {
      await sendEmail({
        to: user.email,
        subject: "Verify your email",
        html: `<a href="${url}">Verify email</a>`,
      })
    },
    expiresIn: 3_600,  // 1 hour
  },

  // Password reset
  emailAndPassword: {
    enabled: true,
    sendResetPassword: async ({ user, url }) => {
      await sendEmail({
        to: user.email,
        subject: "Reset your password",
        html: `<a href="${url}">Reset password</a>`,
      })
    },
  },

  // OAuth providers
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      scope: ["openid", "email", "profile"],
    },
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
  },

  // Plugins
  plugins: [
    organization({
      allowUserToCreateOrganization: true,
      invitationExpiresIn: 48 * 3_600,  // 48 hours
      sendInvitationEmail: async ({ invitation, inviteLink }) => {
        await sendEmail({
          to: invitation.email,
          subject: "You've been invited",
          html: `<a href="${inviteLink}">Accept invitation</a>`,
        })
      },
    }),
    twoFactor({
      otpOptions: { digits: 6 },
    }),
  ],

  // Trusted origins for CORS
  trustedOrigins: [
    process.env.NEXT_PUBLIC_APP_URL!,
  ],

  // Additional user fields
  user: {
    additionalFields: {
      role: {
        type: "string",
        defaultValue: "user",
        input: false,  // Not set by user
      },
      stripeCustomerId: {
        type: "string",
        required: false,
        input: false,
      },
    },
  },
})

export type Session = typeof auth.$Infer.Session
export type User = typeof auth.$Infer.Session.user

async function sendEmail(_opts: { to: string; subject: string; html: string }) {
  // Your email sending implementation
}

Next.js Route Handler

// app/api/auth/[...all]/route.ts — catch-all auth handler
import { auth } from "@/lib/auth"
import { toNextJsHandler } from "better-auth/next-js"

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

Auth Client

// lib/auth-client.ts — browser auth client
import { createAuthClient } from "better-auth/react"
import { organizationClient, twoFactorClient } from "better-auth/client/plugins"

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
  plugins: [
    organizationClient(),
    twoFactorClient({
      twoFactorPage: "/auth/two-factor",
    }),
  ],
})

// Re-export typed hooks and methods
export const {
  signIn,
  signUp,
  signOut,
  useSession,
  getSession,
} = authClient

React Components

// components/auth/SignInForm.tsx — email/password sign in
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { signIn } from "@/lib/auth-client"

export function SignInForm() {
  const router = useRouter()
  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState(false)

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    setError(null)
    setLoading(true)

    const formData = new FormData(e.currentTarget)
    const { error } = await signIn.email({
      email: formData.get("email") as string,
      password: formData.get("password") as string,
      callbackURL: "/dashboard",
    })

    if (error) {
      setError(error.message ?? "Sign in failed")
      setLoading(false)
    } else {
      router.push("/dashboard")
    }
  }

  async function handleGoogleSignIn() {
    await signIn.social({
      provider: "google",
      callbackURL: "/dashboard",
    })
  }

  return (
    <div className="space-y-6">
      <form onSubmit={handleSubmit} className="space-y-4">
        <input name="email" type="email" required placeholder="Email" className="input" />
        <input name="password" type="password" required placeholder="Password" className="input" />
        {error && <p className="text-sm text-red-500">{error}</p>}
        <button type="submit" disabled={loading} className="btn-primary w-full">
          {loading ? "Signing in..." : "Sign in"}
        </button>
      </form>

      <div className="relative">
        <hr />
        <span className="absolute inset-x-0 -top-2.5 text-center text-xs text-muted-foreground bg-background px-2 mx-auto w-8">or</span>
      </div>

      <button onClick={handleGoogleSignIn} className="btn-outline w-full">
        Continue with Google
      </button>
    </div>
  )
}

// components/auth/SessionProvider.tsx — access session in components
"use client"
import { useSession } from "@/lib/auth-client"
import { redirect } from "next/navigation"

export function RequireAuth({ children }: { children: React.ReactNode }) {
  const { data: session, isPending } = useSession()

  if (isPending) return <div>Loading...</div>
  if (!session) redirect("/auth/sign-in")

  return <>{children}</>
}

Server-Side Session + Middleware

// middleware.ts — protect routes
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { auth } from "@/lib/auth"

const PUBLIC_PATHS = ["/", "/auth/sign-in", "/auth/sign-up", "/api/auth"]

export async function middleware(request: NextRequest) {
  const isPublic = PUBLIC_PATHS.some(p => request.nextUrl.pathname.startsWith(p))
  if (isPublic) return NextResponse.next()

  const session = await auth.api.getSession({
    headers: request.headers,
  })

  if (!session) {
    const signInUrl = new URL("/auth/sign-in", request.url)
    signInUrl.searchParams.set("callbackURL", request.nextUrl.pathname)
    return NextResponse.redirect(signInUrl)
  }

  // Role-based access
  if (request.nextUrl.pathname.startsWith("/admin")) {
    if (session.user.role !== "admin") {
      return NextResponse.redirect(new URL("/403", request.url))
    }
  }

  return NextResponse.next()
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
}
// app/dashboard/page.tsx — server-side session in Server Component
import { headers } from "next/headers"
import { auth } from "@/lib/auth"
import { redirect } from "next/navigation"

export default async function DashboardPage() {
  const session = await auth.api.getSession({
    headers: await headers(),
  })

  if (!session) redirect("/auth/sign-in")

  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
      <p className="text-muted-foreground">{session.user.email}</p>
    </div>
  )
}

For the NextAuth.js (Auth.js) alternative when a more established ecosystem with a larger plugin library (25+ providers) and official framework adapters for SvelteKit, Express, and Fastify is needed — Auth.js v5 has a similar server-first design with auth() for Server Components and middleware, see the authentication comparison guide. For the Lucia alternative when a lower-level auth toolkit with no magic — just session management helpers and database adapters — is preferred to build a completely custom auth flow without any pre-built email/OAuth strategies, see the custom authentication guide. The Claude Skills 360 bundle includes Better Auth skill sets covering email/password, OAuth, organizations, and session middleware. Start with the free tier to try authentication generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free