
better-auth
by 5dlabs
Cognitive Task Orchestrator - GitOps on Bare Metal or Cloud for AI Agents
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'],
}
Popular Plugins
| Plugin | Purpose | Install |
|---|---|---|
twoFactor | 2FA with TOTP, backup codes | Built-in |
organization | Multi-tenant, teams, roles | Built-in |
passkey | WebAuthn/Passkey support | Built-in |
magicLink | Email magic link auth | Built-in |
apiKey | API key authentication | Built-in |
username | Username-based auth | Built-in |
admin | Admin panel, user management | Built-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 })
Magic Link (Passwordless)
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
- Always use
nextCookies()plugin in Next.js for Server Actions - Validate sessions server-side for protected actions (don't trust cookie existence)
- Use Effect for type-safe auth errors in backend services
- Store secrets in environment variables, never commit them
- Run database migrations after adding/changing plugins
- Use social sign-on for better UX where appropriate
Documentation
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon


