name: better-auth-skill description: Expert Better Auth skill with production best practices, session management, security hardening, and deployment optimization. Use with Better Auth MCP server.
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
BETTER_AUTH_SECRETis set in your hosting platform (Vercel, Netlify, etc.) - Database Indexes: Create indexes on
sessions.token,sessions.userId,users.emailfor performance - Dynamic Pages: Add
export const dynamic = 'force-dynamic';to protected pages to prevent static generation - 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
dynamic = 'force-dynamic'to the page - Sign-in works but session not persisted: Check that
BETTER_AUTH_SECRETis the same in all environments - 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