Back to list
5dlabs

better-auth

by 5dlabs

Cognitive Task Orchestrator - GitOps on Bare Metal or Cloud for AI Agents

2🍴 1📅 Jan 24, 2026

SKILL.md


name: better-auth description: Universal TypeScript authentication framework for Node.js backends and React frontends agents: [blaze, nova, rex, grizz] triggers: [auth, login, session, signin, signup, oauth, 2fa, passkey, organization] context7_libraries:

  • /better-auth/better-auth llm_docs:
  • better-auth

Better Auth

Better Auth (better-auth.com) is the most comprehensive authentication framework for TypeScript. Use it for all authentication needs in Node.js backends and React frontends.

AI Tooling

IMPORTANT: Before implementing Better Auth, consult:

  • AI Documentation: https://better-auth.com/llms.txt
  • MCP Server: Available via https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp

Use Context7 to look up Better Auth patterns:

resolve_library_id({ libraryName: "better-auth typescript" })
get_library_docs({ context7CompatibleLibraryID: "/better-auth/better-auth", topic: "installation setup" })

Context7 Better Auth Topics

get_library_docs({ libraryName: "better-auth", topic: "elysia integration" })
get_library_docs({ libraryName: "better-auth", topic: "next.js integration" })
get_library_docs({ libraryName: "better-auth", topic: "two factor authentication" })
get_library_docs({ libraryName: "better-auth", topic: "organization plugin" })
get_library_docs({ libraryName: "better-auth", topic: "session management" })

Installation

# Install Better Auth
bun add better-auth  # Backend
pnpm add better-auth # Frontend

Environment Variables

# .env
BETTER_AUTH_SECRET=your-secret-key-at-least-32-chars  # Generate with: openssl rand -base64 32
BETTER_AUTH_URL=http://localhost:3000                 # Base URL of your app

Backend Integration (Elysia)

Server Configuration

// lib/auth.ts
import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { db } from "@/db"  // your Drizzle instance

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",  // or "mysql", "sqlite"
  }),
  emailAndPassword: {
    enabled: true,
    autoSignIn: true,  // Auto sign-in after registration
  },
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    },
  },
})

Mount Handler in Elysia

// src/index.ts
import { Elysia } from "elysia"
import { cors } from "@elysiajs/cors"
import { auth } from "./lib/auth"

const app = new Elysia()
  .use(cors({
    origin: process.env.FRONTEND_URL || "http://localhost:3001",
    methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    credentials: true,
    allowedHeaders: ["Content-Type", "Authorization"],
  }))
  .mount(auth.handler)
  .listen(3000)

console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`)

Auth Macro for Protected Routes

import { Elysia } from "elysia"
import { auth } from "./lib/auth"

// Auth middleware using Elysia macro
const betterAuthPlugin = new Elysia({ name: "better-auth" })
  .mount(auth.handler)
  .macro({
    auth: {
      async resolve({ status, request: { headers } }) {
        const session = await auth.api.getSession({ headers })

        if (!session) return status(401)

        return {
          user: session.user,
          session: session.session,
        }
      },
    },
  })

// Use in routes
const app = new Elysia()
  .use(betterAuthPlugin)
  .get("/api/me", ({ user }) => user, { auth: true })
  .get("/api/protected", ({ user, session }) => ({
    message: `Hello ${user.name}!`,
    sessionId: session.id,
  }), { auth: true })

Effect Integration with Better Auth

import { Effect, Context, Layer, Schema } from "effect"
import { auth } from "./lib/auth"

// Auth service definition
class AuthService extends Context.Tag("AuthService")<
  AuthService,
  {
    getSession: (headers: Headers) => Effect.Effect<Session | null, never>
    signIn: (email: string, password: string) => Effect.Effect<Session, AuthError>
    signOut: (headers: Headers) => Effect.Effect<void, never>
  }
>() {}

// Auth errors
class AuthError extends Schema.TaggedError<AuthError>("AuthError")({
  message: Schema.String,
  code: Schema.String,
}) {}

// Live implementation
const AuthServiceLive = Layer.succeed(
  AuthService,
  AuthService.of({
    getSession: (headers) => Effect.promise(() => auth.api.getSession({ headers })),
    signIn: (email, password) => Effect.tryPromise({
      try: () => auth.api.signInEmail({ body: { email, password } }),
      catch: (e) => new AuthError({ message: String(e), code: "SIGN_IN_FAILED" }),
    }),
    signOut: (headers) => Effect.promise(() => auth.api.signOut({ headers })).pipe(Effect.asVoid),
  })
)

Frontend Integration (Next.js)

API Route

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

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

Server Configuration

// lib/auth.ts
import { betterAuth } from "better-auth"
import { nextCookies } from "better-auth/next-js"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { db } from "@/db"

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
  }),
  emailAndPassword: {
    enabled: true,
  },
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
  },
  plugins: [nextCookies()],  // IMPORTANT: Must be last plugin
})

Auth Client

// lib/auth-client.ts
import { createAuthClient } from "better-auth/react"

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_API_URL || "",  // Same domain = empty string
})

// Export individual methods for convenience
export const { signIn, signUp, signOut, useSession } = authClient

Sign Up Component

'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { authClient } from '@/lib/auth-client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'

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

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

    const formData = new FormData(e.currentTarget)
    
    const { error } = await authClient.signUp.email({
      email: formData.get('email') as string,
      password: formData.get('password') as string,
      name: formData.get('name') as string,
      callbackURL: '/dashboard',
    }, {
      onSuccess: () => router.push('/dashboard'),
      onError: (ctx) => setError(ctx.error.message),
    })
    
    setLoading(false)
  }

  return (
    <Card className="w-full max-w-md">
      <CardHeader>
        <CardTitle>Create Account</CardTitle>
      </CardHeader>
      <CardContent>
        <form onSubmit={handleSubmit} className="space-y-4">
          <div>
            <Label htmlFor="name">Name</Label>
            <Input id="name" name="name" required />
          </div>
          <div>
            <Label htmlFor="email">Email</Label>
            <Input id="email" name="email" type="email" required />
          </div>
          <div>
            <Label htmlFor="password">Password</Label>
            <Input id="password" name="password" type="password" minLength={8} required />
          </div>
          {error && <p className="text-sm text-destructive">{error}</p>}
          <Button type="submit" className="w-full" disabled={loading}>
            {loading ? 'Creating account...' : 'Sign Up'}
          </Button>
        </form>
      </CardContent>
    </Card>
  )
}

Sign In Component

'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { authClient } from '@/lib/auth-client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'

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

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

    const formData = new FormData(e.currentTarget)
    
    await authClient.signIn.email({
      email: formData.get('email') as string,
      password: formData.get('password') as string,
      callbackURL: '/dashboard',
    }, {
      onSuccess: () => router.push('/dashboard'),
      onError: (ctx) => setError(ctx.error.message),
    })
    
    setLoading(false)
  }

  async function handleSocialSignIn(provider: 'github' | 'google') {
    await authClient.signIn.social({
      provider,
      callbackURL: '/dashboard',
    })
  }

  return (
    <div className="space-y-4">
      <form onSubmit={handleEmailSignIn} className="space-y-4">
        <div>
          <Label htmlFor="email">Email</Label>
          <Input id="email" name="email" type="email" required />
        </div>
        <div>
          <Label htmlFor="password">Password</Label>
          <Input id="password" name="password" type="password" required />
        </div>
        {error && <p className="text-sm text-destructive">{error}</p>}
        <Button type="submit" className="w-full" disabled={loading}>
          {loading ? 'Signing in...' : 'Sign In'}
        </Button>
      </form>

      <Separator />

      <div className="space-y-2">
        <Button 
          variant="outline" 
          className="w-full" 
          onClick={() => handleSocialSignIn('github')}
        >
          Continue with GitHub
        </Button>
        <Button 
          variant="outline" 
          className="w-full" 
          onClick={() => handleSocialSignIn('google')}
        >
          Continue with Google
        </Button>
      </div>
    </div>
  )
}

Session Hook Usage

'use client'

import { authClient } from '@/lib/auth-client'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'

export function UserMenu() {
  const { data: session, isPending } = authClient.useSession()

  if (isPending) {
    return <div className="h-8 w-8 animate-pulse rounded-full bg-muted" />
  }

  if (!session) {
    return <Button variant="outline" asChild><a href="/sign-in">Sign In</a></Button>
  }

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" className="relative h-8 w-8 rounded-full">
          <Avatar className="h-8 w-8">
            <AvatarImage src={session.user.image || ''} alt={session.user.name} />
            <AvatarFallback>{session.user.name?.[0]?.toUpperCase()}</AvatarFallback>
          </Avatar>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem className="font-medium">{session.user.name}</DropdownMenuItem>
        <DropdownMenuItem className="text-muted-foreground">{session.user.email}</DropdownMenuItem>
        <DropdownMenuItem onClick={() => authClient.signOut()}>
          Sign Out
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Server-Side Session (RSC)

// app/dashboard/page.tsx
import { auth } from '@/lib/auth'
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'

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

  if (!session) {
    redirect('/sign-in')
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}!</h1>
      <p>Email: {session.user.email}</p>
    </div>
  )
}

Middleware Protection

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { getSessionCookie } from 'better-auth/cookies'

const protectedRoutes = ['/dashboard', '/settings', '/profile']
const authRoutes = ['/sign-in', '/sign-up']

export function middleware(request: NextRequest) {
  const sessionCookie = getSessionCookie(request)
  const isProtected = protectedRoutes.some(route => 
    request.nextUrl.pathname.startsWith(route)
  )
  const isAuthRoute = authRoutes.some(route => 
    request.nextUrl.pathname.startsWith(route)
  )

  // Redirect to sign-in if accessing protected route without session
  if (isProtected && !sessionCookie) {
    return NextResponse.redirect(new URL('/sign-in', request.url))
  }

  // Redirect to dashboard if accessing auth route with session
  if (isAuthRoute && sessionCookie) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*', '/profile/:path*', '/sign-in', '/sign-up'],
}

PluginPurposeInstall
twoFactor2FA with TOTP, backup codesBuilt-in
organizationMulti-tenant, teams, rolesBuilt-in
passkeyWebAuthn/Passkey supportBuilt-in
magicLinkEmail magic link authBuilt-in
apiKeyAPI key authenticationBuilt-in
usernameUsername-based authBuilt-in
adminAdmin panel, user managementBuilt-in

Two-Factor Authentication (2FA)

Server

import { betterAuth } from "better-auth"
import { twoFactor } from "better-auth/plugins"

export const auth = betterAuth({
  plugins: [
    twoFactor({
      issuer: "MyApp",  // Shown in authenticator apps
      totpOptions: {
        digits: 6,
        period: 30,
      },
    }),
  ],
})

Client

import { createAuthClient } from "better-auth/react"
import { twoFactorClient } from "better-auth/client/plugins"

export const authClient = createAuthClient({
  plugins: [
    twoFactorClient({
      twoFactorPage: "/two-factor",  // Redirect here for 2FA verification
    }),
  ],
})

// Enable 2FA
await authClient.twoFactor.enable({ password: "user-password" })

// Verify TOTP code
await authClient.twoFactor.verifyTOTP({ 
  code: "123456",
  trustDevice: true,  // Skip 2FA on this device next time
})

// Generate backup codes
const { data } = await authClient.twoFactor.generateBackupCodes()

Organization (Multi-Tenant)

Server

import { betterAuth } from "better-auth"
import { organization } from "better-auth/plugins"

export const auth = betterAuth({
  plugins: [
    organization({
      allowUserToCreateOrganization: true,
      organizationLimit: 5,  // Max orgs per user
      membershipLimit: 100,  // Max members per org
      roles: {
        owner: { permissions: ["*"] },
        admin: { permissions: ["invite", "remove", "update"] },
        member: { permissions: ["read"] },
      },
    }),
  ],
})

Client

import { organizationClient } from "better-auth/client/plugins"

const authClient = createAuthClient({
  plugins: [organizationClient()],
})

// Create organization
const { data: org } = await authClient.organization.create({
  name: "My Company",
  slug: "my-company",
})

// Invite member
await authClient.organization.inviteMember({
  organizationId: org.id,
  email: "user@example.com",
  role: "member",
})

// List user's organizations
const { data: orgs } = await authClient.organization.list()

// Switch active organization
await authClient.organization.setActive({ organizationId: org.id })

Passkey (WebAuthn)

Server

import { betterAuth } from "better-auth"
import { passkey } from "better-auth/plugins"

export const auth = betterAuth({
  plugins: [
    passkey({
      rpID: "myapp.com",  // Relying Party ID (your domain)
      rpName: "My App",
      origin: "https://myapp.com",
    }),
  ],
})

Client

import { passkeyClient } from "better-auth/client/plugins"

const authClient = createAuthClient({
  plugins: [passkeyClient()],
})

// Register passkey
await authClient.passkey.addPasskey()

// Sign in with passkey
await authClient.signIn.passkey()

// List user's passkeys
const { data: passkeys } = await authClient.passkey.listPasskeys()

// Delete passkey
await authClient.passkey.deletePasskey({ id: passkeyId })

API Keys (Machine-to-Machine)

Server

import { betterAuth } from "better-auth"
import { apiKey } from "better-auth/plugins"

export const auth = betterAuth({
  plugins: [
    apiKey({
      rateLimit: {
        window: 60,  // 60 seconds
        max: 100,    // 100 requests per window
      },
    }),
  ],
})

// Validate API key in your routes
app.get("/api/data", async (req) => {
  const key = req.headers["x-api-key"]
  const result = await auth.api.verifyApiKey({ key })
  if (!result.valid) return { error: "Invalid API key" }
  // result.userId contains the key owner
})

Client

import { apiKeyClient } from "better-auth/client/plugins"

const authClient = createAuthClient({
  plugins: [apiKeyClient()],
})

// Create API key
const { data } = await authClient.apiKey.create({
  name: "My Integration",
  expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),  // 30 days
})
// data.key is the API key (only shown once!)

// List API keys
const { data: keys } = await authClient.apiKey.list()

// Revoke API key
await authClient.apiKey.revoke({ id: keyId })

Server

import { betterAuth } from "better-auth"
import { magicLink } from "better-auth/plugins"

export const auth = betterAuth({
  plugins: [
    magicLink({
      sendMagicLink: async ({ email, url }) => {
        await sendEmail({
          to: email,
          subject: "Sign in to MyApp",
          html: `<a href="${url}">Click to sign in</a>`,
        })
      },
      expiresIn: 60 * 10,  // 10 minutes
    }),
  ],
})

Client

import { magicLinkClient } from "better-auth/client/plugins"

const authClient = createAuthClient({
  plugins: [magicLinkClient()],
})

// Send magic link
await authClient.signIn.magicLink({
  email: "user@example.com",
  callbackURL: "/dashboard",
})

Admin Plugin

Server

import { betterAuth } from "better-auth"
import { admin } from "better-auth/plugins"

export const auth = betterAuth({
  plugins: [
    admin({
      adminRoles: ["admin", "superadmin"],
    }),
  ],
})

Client

import { adminClient } from "better-auth/client/plugins"

const authClient = createAuthClient({
  plugins: [adminClient()],
})

// List all users (admin only)
const { data: users } = await authClient.admin.listUsers({
  limit: 50,
  offset: 0,
})

// Ban user
await authClient.admin.banUser({ userId: "..." })

// Impersonate user
await authClient.admin.impersonateUser({ userId: "..." })

// Stop impersonation
await authClient.admin.stopImpersonation()

Combining Multiple Plugins

import { betterAuth } from "better-auth"
import { 
  twoFactor, 
  organization, 
  passkey, 
  apiKey,
  admin 
} from "better-auth/plugins"

export const auth = betterAuth({
  database: drizzleAdapter(db, { provider: "pg" }),
  emailAndPassword: { enabled: true },
  plugins: [
    twoFactor({ issuer: "MyApp" }),
    organization({ allowUserToCreateOrganization: true }),
    passkey({ rpID: "myapp.com", rpName: "MyApp" }),
    apiKey(),
    admin({ adminRoles: ["admin"] }),
  ],
})

Database Migration

After configuring Better Auth (especially with plugins), run migrations:

# Generate migration file
npx @better-auth/cli generate

# Apply migration directly (Kysely adapter only)
npx @better-auth/cli migrate

MCP Server

Better Auth provides an MCP server for AI-assisted development:

{
  "mcpServers": {
    "better-auth": {
      "url": "https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp"
    }
  }
}

Or add via CLI:

npx @better-auth/cli mcp --claude-code

Best Practices

  1. Always use nextCookies() plugin in Next.js for Server Actions
  2. Validate sessions server-side for protected actions (don't trust cookie existence)
  3. Use Effect for type-safe auth errors in backend services
  4. Store secrets in environment variables, never commit them
  5. Run database migrations after adding/changing plugins
  6. Use social sign-on for better UX where appropriate

Documentation

Score

Total Score

65/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon