---
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):
```typescript
// 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
```bash
# .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
```typescript
// 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)
```typescript
// 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
```typescript
// 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 (
Dashboard - Welcome {session.user.name}
{/* Your protected content */}
);
}
```
## Server Actions with Session Validation
```typescript
// 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+)
```typescript
// 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)
```typescript
// 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
```typescript
// 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
```typescript
// 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:
```typescript
// List all knowledge base files
@better-auth:list_files
```
Use `@better-auth:search` to find specific topics:
```typescript
// Search for session configuration
@better-auth:search query="session management configuration"
```
## Production Deployment Best Practices
1. **Set Environment Variables**: Ensure `BETTER_AUTH_SECRET` is set in your hosting platform (Vercel, Netlify, etc.)
2. **Database Indexes**: Create indexes on `sessions.token`, `sessions.userId`, `users.email` for performance
3. **Dynamic Pages**: Add `export const dynamic = 'force-dynamic';` to protected pages to prevent static generation
4. **Cookie Configuration**: Use appropriate cookie settings for production (secure, sameSite, httpOnly)
5. **Base URL Configuration**: Set correct baseURL for your production environment
6. **Secret Validation**: Ensure the same secret is used across all runtimes (Edge, Serverless)
7. **Trusted Origins**: Include all domains where your app will be hosted
8. **Session Validation**: Always validate sessions server-side for protected routes
9. **Rate Limiting**: Enable rate limiting in production to prevent abuse
10. **Logging**: Set appropriate log levels for production (error/warn)
## Common Production Issues and Fixes
1. **Dashboard redirects to login despite valid session**: Add `dynamic = 'force-dynamic'` to the page
2. **Sign-in works but session not persisted**: Check that `BETTER_AUTH_SECRET` is the same in all environments
3. **Cookies not working in production**: Verify cookie attributes (secure, sameSite) are appropriate for HTTPS
4. **Middleware blocking access**: Use Node.js runtime for full validation or presence-only check for Edge
5. **Static generation issues**: Mark protected routes as dynamic to prevent pre-rendering
6. **Slow session lookups**: Add database indexes on session and user tables
7. **Session validation fails in middleware**: Pass headers correctly to auth.api.getSession()
## Database Optimization
```sql
-- 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