Claude-skill-registry better-auth-skill
Expert Better Auth skill with production best practices, session management, security hardening, and deployment optimization. Use with Better Auth MCP server.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/better-auth-skill" ~/.claude/skills/majiayu000-claude-skill-registry-better-auth-skill && rm -rf "$T"
manifest:
skills/data/better-auth-skill/SKILL.mdsource content
Better Auth Skill
Expert skill for implementing Better Auth in Next.js applications with production best practices, security hardening, and deployment optimization.
Production-Ready Configuration
Use this configuration for production deployments (especially on Vercel):
// lib/auth.ts - Production Ready import { betterAuth } from "better-auth"; import { nextCookies } from "better-auth/next-js"; import { Pool } from "pg"; // or your database client // Create a singleton pool to prevent multiple connections during dev const globalForPool = globalThis as unknown as { pool: Pool | undefined }; export const pool = globalForPool.pool ?? new Pool({ connectionString: process.env.DATABASE_URL, ssl: process.env.NODE_ENV === "production" ? { rejectUnauthorized: false } : undefined // No SSL in development unless required }); if (process.env.NODE_ENV !== "production") globalForPool.pool = pool; // Ensure the secret is properly set - fail loudly if missing const authSecret = process.env.BETTER_AUTH_SECRET; if (!authSecret) { console.error("BETTER_AUTH_SECRET is not set! This will cause authentication to fail."); if (process.env.NODE_ENV === "production") { throw new Error("BETTER_AUTH_SECRET environment variable is required in production"); } } export const auth = betterAuth({ secret: authSecret || "dev-secret-for-development-only-change-in-production", baseURL: process.env.BETTER_AUTH_URL || (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : process.env.VERCEL_BRANCH_URL ? `https://${process.env.VERCEL_BRANCH_URL}` : "http://localhost:3000"), database: pool, // or your database configuration trustedOrigins: [ "https://your-production-domain.vercel.app", // Production Vercel URL `https://your-project-git-*vercel.app`, // Vercel preview URLs "http://localhost:3000", // Local development "http://127.0.0.1:3000", // Alternative local address ], advanced: { useSecureCookies: process.env.NODE_ENV === "production", // Force secure cookies in production cookiePrefix: "yourapp", // Reduce fingerprinting session: { expiresIn: 7 * 24 * 60 * 60, // 7 days in seconds updateAge: 24 * 60 * 60, // Update session every 24 hours freshAge: 15 * 60, // 15 minutes for sensitive operations cookieCache: { enabled: true, maxAge: 300, // 5 minutes cache strategy: "compact", // smallest and fastest refreshCache: true // Refresh when updateAge threshold reached } }, defaultCookieAttributes: { // Set default attributes for all cookies httpOnly: true, secure: process.env.NODE_ENV === "production", // Secure in production sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", // "none" for cross-site in production with secure path: "/", }, ipAddress: { ipAddressHeaders: ["cf-connecting-ip"] // Cloudflare header, adjust for your proxy } }, account: { encryptOAuthTokens: true, // Encrypt OAuth tokens at rest storeStateStrategy: "database" // Default safe strategy }, rateLimit: { enabled: process.env.NODE_ENV === "production" // Enable in production }, logger: { level: process.env.NODE_ENV === "production" ? "error" : "debug" }, plugins: [ nextCookies() // Install this plugin last to ensure Set-Cookie is applied correctly ], });
Environment Variables
# .env.local BETTER_AUTH_SECRET="your-32-character-secret-key-minimum" BETTER_AUTH_URL="http://localhost:3000" NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000" DATABASE_URL="postgresql://username:password@host:port/database" # For Vercel deployment VERCEL_ENV="production" # Will be set automatically by Vercel VERCEL_URL="your-project-name.vercel.app" # Set automatically by Vercel
Client-Side Configuration
// lib/auth-client.ts import { createAuthClient } from "better-auth/react"; import { jwtClient } from "better-auth/client/plugins"; // Determine the correct base URL based on environment const getBaseURL = () => { if (typeof window !== "undefined") { // Browser environment return process.env.NEXT_PUBLIC_BETTER_AUTH_URL || (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"); } else { // Server environment return process.env.BETTER_AUTH_URL || (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"); } }; export const authClient = createAuthClient({ baseURL: getBaseURL(), plugins: [ jwtClient() ] }); export const { signIn, signUp, signOut, useSession } = authClient;
API Route Handler (App Router)
// app/api/auth/[...all]/route.ts import { auth } from "@/lib/auth"; import { toNextJsHandler } from "better-auth/next-js"; export const { GET, POST } = toNextJsHandler(auth.handler);
Server Component Session Validation
// app/dashboard/page.tsx import { redirect } from "next/navigation"; import { headers } from "next/headers"; import { auth } from "@/lib/auth"; // Force this page to be dynamic to prevent static generation export const dynamic = 'force-dynamic'; export default async function DashboardPage() { const session = await auth.api.getSession({ headers: await headers(), // Always pass headers from server components }); if (!session) { redirect("/login"); } return ( <div> <h1>Dashboard - Welcome {session.user.name}</h1> {/* Your protected content */} </div> ); }
Server Actions with Session Validation
// lib/actions.ts "use server"; import { auth } from "@/lib/auth"; import { headers } from "next/headers"; export async function getUserProfile() { const session = await auth.api.getSession({ headers: await headers(), // Always pass headers from server actions }); if (!session) { throw new Error("Unauthorized"); } return session.user; }
Next.js 16+ Proxy for Authentication (Recommended for Next 16+)
// proxy.ts (Next.js 16+) import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import { auth } from "@/lib/auth"; // Public routes that don't require authentication const publicRoutes = ["/", "/login", "/register"]; export async function middleware(req: NextRequest) { // For full validation, use Node.js runtime // For optimistic redirects, use presence-only check const session = await auth.api.getSession({ headers: { cookie: req.headers.get("cookie") || "", }, }); const isLoggedIn = !!session?.user; const isOnPublicRoute = publicRoutes.includes(req.nextUrl.pathname); // If on a protected route without being logged in, redirect to login if (!isOnPublicRoute && !isLoggedIn) { return NextResponse.redirect(new URL("/login", req.url)); } // If logged in and trying to access login/register, redirect to dashboard if ((req.nextUrl.pathname === "/login" || req.nextUrl.pathname === "/register") && isLoggedIn) { return NextResponse.redirect(new URL("/dashboard", req.url)); } return NextResponse.next(); } // Use Node.js runtime for full session validation export const config = { runtime: "nodejs", matcher: [ /* * Match all request paths except for: * - api routes (handled by Better Auth API) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) * - public folder */ "/((?!api|_next/static|_next/image|favicon.ico|public).*)", ], };
Next.js Edge Middleware (For older Next.js versions or when Node runtime not available)
// middleware.ts (Edge runtime) import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import { getSessionCookie } from "better-auth/cookies"; // Public routes that don't require authentication const publicRoutes = ["/", "/login", "/register"]; export function middleware(req: NextRequest) { // Edge runtime cannot make database calls // Use presence-only check for optimistic redirects const sessionToken = getSessionCookie(req); const hasSessionCookie = !!sessionToken; const isOnPublicRoute = publicRoutes.includes(req.nextUrl.pathname); // If on a protected route without any session cookie, redirect to login // NOTE: This is NOT a security check, only for UX if (!isOnPublicRoute && !hasSessionCookie) { return NextResponse.redirect(new URL("/login", req.url)); } // If logged in (has cookie) and trying to access login/register, redirect to dashboard if ((req.nextUrl.pathname === "/login" || req.nextUrl.pathname === "/register") && hasSessionCookie) { return NextResponse.redirect(new URL("/dashboard", req.url)); } return NextResponse.next(); } export const config = { matcher: [ /* * Match all request paths except for: * - api routes (handled by Better Auth API) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) * - public folder */ "/((?!api|_next/static|_next/image|favicon.ico|public).*)", ], };
Session Management Best Practices
// lib/session-utils.ts import { auth } from "@/lib/auth"; import { headers } from "next/headers"; // Get session with strict validation (forces DB lookup) export async function getStrictSession() { const session = await auth.api.getSession({ headers: await headers(), query: { disableCookieCache: true // Forces database validation } }); return session; } // Revoke session (for logout, security operations) export async function revokeCurrentSession() { const session = await auth.api.getSession({ headers: await headers(), }); if (session) { await auth.api.revokeSession({ sessionId: session.session.id, headers: await headers(), }); } } // Revoke all other sessions (for password change, security) export async function revokeOtherSessions() { const session = await auth.api.getSession({ headers: await headers(), }); if (session) { await auth.api.revokeOtherSessions({ userId: session.user.id, headers: await headers(), }); } }
Security Hardening
// Additional security measures in auth configuration advanced: { // Disable CSRF and origin checks only if you know exactly why (NEVER in production) // disableCSRFCheck: false, // Default and recommended // disableOriginCheck: false, // Default and recommended // Additional security settings useSecureCookies: process.env.NODE_ENV === "production", cookiePrefix: "yourapp", defaultCookieAttributes: { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", path: "/", } }, rateLimit: { enabled: process.env.NODE_ENV === "production", window: 60 * 1000, // 1 minute window max: 10, // 10 attempts per window // Stricter limits for sensitive routes overrides: { signIn: { max: 5 }, forgotPassword: { max: 3 }, } }, logger: { level: process.env.NODE_ENV === "production" ? "warn" : "debug" }
Better Auth MCP Usage
Use
@better-auth:list_files to see available documentation:
// List all knowledge base files @better-auth:list_files
Use
@better-auth:search to find specific topics:
// Search for session configuration @better-auth:search query="session management configuration"
Production Deployment Best Practices
- Set Environment Variables: Ensure
is set in your hosting platform (Vercel, Netlify, etc.)BETTER_AUTH_SECRET - Database Indexes: Create indexes on
,sessions.token
,sessions.userId
for performanceusers.email - Dynamic Pages: Add
to protected pages to prevent static generationexport const dynamic = 'force-dynamic'; - Cookie Configuration: Use appropriate cookie settings for production (secure, sameSite, httpOnly)
- Base URL Configuration: Set correct baseURL for your production environment
- Secret Validation: Ensure the same secret is used across all runtimes (Edge, Serverless)
- Trusted Origins: Include all domains where your app will be hosted
- Session Validation: Always validate sessions server-side for protected routes
- Rate Limiting: Enable rate limiting in production to prevent abuse
- Logging: Set appropriate log levels for production (error/warn)
Common Production Issues and Fixes
- Dashboard redirects to login despite valid session: Add
to the pagedynamic = 'force-dynamic' - Sign-in works but session not persisted: Check that
is the same in all environmentsBETTER_AUTH_SECRET - Cookies not working in production: Verify cookie attributes (secure, sameSite) are appropriate for HTTPS
- Middleware blocking access: Use Node.js runtime for full validation or presence-only check for Edge
- Static generation issues: Mark protected routes as dynamic to prevent pre-rendering
- Slow session lookups: Add database indexes on session and user tables
- Session validation fails in middleware: Pass headers correctly to auth.api.getSession()
Database Optimization
-- Create indexes for better performance CREATE INDEX idx_sessions_token ON sessions(token); CREATE INDEX idx_sessions_user_id ON sessions(user_id); CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_accounts_user_id ON accounts(user_id); CREATE INDEX idx_verifications_identifier ON verifications(identifier);
Troubleshooting Checklist
- BETTER_AUTH_SECRET is set in production environment
- Page is marked as dynamic if it checks for session
- Cookie attributes are configured for production (secure, sameSite, httpOnly)
- Trusted origins include production domain
- BaseURL is configured correctly for production
- Middleware passes headers correctly to auth.api.getSession()
- nextCookies plugin is installed and positioned last in plugins
- Database indexes are created for performance
- Redeploy after environment variable changes
- Runtime is set to "nodejs" in middleware if full validation is needed
- CSRF and origin checks are enabled in production
- Rate limiting is enabled in production