auth-better-auth
Better Auth Skill
Integration patterns for connecting to Better Auth in Next.js 16 projects using the Drizzle adapter.
When to Use This Skill
- Connecting a Next.js app to Better Auth
- Configuring OAuth providers (Google, GitHub, etc.)
- Implementing protected routes with Next.js 16 proxy.ts
- Adding auth state to React components
Core Concepts
What Better Auth Provides
Better Auth is a TypeScript-first authentication framework that handles:
- OAuth flows (Google, GitHub, Apple, etc.)
- Session management
- User/account storage
- JWT tokens (optional)
This skill covers connecting to Better Auth, not building the auth service itself.
Setup
Package Installation
npm install better-auth
Environment Variables
Add to .env.local:
# Better Auth
BETTER_AUTH_SECRET=your-secret-key-min-32-chars
BETTER_AUTH_URL=http://localhost:3000
# OAuth Providers
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
Auth Configuration
Server-Side Auth
Create src/lib/auth.ts:
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { nextCookies } from 'better-auth/next-js';
import { db } from '@/lib/db';
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
}),
emailAndPassword: {
enabled: false, // Enable if needed
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
trustedOrigins: [
process.env.BETTER_AUTH_URL || 'http://localhost:3000',
],
plugins: [
nextCookies(), // Must be last plugin
],
});
export type Session = typeof auth.$Infer.Session;
export type User = typeof auth.$Infer.User;
Client-Side Auth
Create src/lib/auth-client.ts:
import { createAuthClient } from 'better-auth/react';
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || 'http://localhost:3000',
});
export const {
signIn,
signUp,
signOut,
useSession,
} = authClient;
API Route Handler
Create src/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);
This handles all auth endpoints:
/api/auth/signin/*- Sign in flows/api/auth/signup- Registration/api/auth/signout- Sign out/api/auth/session- Session info/api/auth/callback/*- OAuth callbacks
Route Protection with proxy.ts
Next.js 16 Proxy (replaces middleware.ts)
Create proxy.ts at project root:
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';
const protectedRoutes = ['/dashboard', '/settings', '/profile'];
const authRoutes = ['/login', '/signup'];
export async function proxy(request: NextRequest): Promise<NextResponse> {
const { pathname } = request.nextUrl;
const isProtectedRoute = protectedRoutes.some((route) =>
pathname.startsWith(route)
);
const isAuthRoute = authRoutes.some((route) =>
pathname.startsWith(route)
);
// Skip auth check for non-protected routes
if (!isProtectedRoute && !isAuthRoute) {
return NextResponse.next();
}
// Get session
const session = await auth.api.getSession({
headers: await headers(),
});
// Redirect unauthenticated users from protected routes
if (isProtectedRoute && !session) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
// Redirect authenticated users from auth routes
if (isAuthRoute && session) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
Quick Cookie Check (Faster, Less Secure)
For performance-critical paths where you only need presence check:
import { getSessionCookie } from 'better-auth/next-js';
export async function proxy(request: NextRequest): Promise<NextResponse> {
// Fast path - just check cookie existence
const sessionCookie = getSessionCookie(request);
if (!sessionCookie && isProtectedRoute(request.nextUrl.pathname)) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
Note: Cookie presence doesn't guarantee valid session. Always validate in API routes.
Server Component Usage
Getting Session in Server Components
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('/login');
}
return (
<div>
<h1>Welcome, {session.user.name}</h1>
<p>Email: {session.user.email}</p>
</div>
);
}
Helper Function
Create src/lib/auth-utils.ts:
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
export async function getSession() {
return auth.api.getSession({
headers: await headers(),
});
}
export async function requireSession() {
const session = await getSession();
if (!session) {
redirect('/login');
}
return session;
}
Usage:
export default async function SettingsPage() {
const session = await requireSession();
return <SettingsForm user={session.user} />;
}
Client Component Usage
Session Hook
'use client';
import { useSession, signOut } from '@/lib/auth-client';
export function UserMenu() {
const { data: session, isPending } = useSession();
if (isPending) {
return <Skeleton className="h-8 w-8 rounded-full" />;
}
if (!session) {
return <a href="/login">Sign In</a>;
}
return (
<DropdownMenu>
<DropdownMenuTrigger>
<Avatar>
<AvatarImage src={session.user.image} />
<AvatarFallback>{session.user.name?.[0]}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => signOut()}>
Sign Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Sign In Buttons
'use client';
import { signIn } from '@/lib/auth-client';
export function LoginPage() {
return (
<div className="flex flex-col gap-4">
<h1>Sign In</h1>
<button
onClick={() => signIn.social({ provider: 'google' })}
className="btn btn-outline"
>
Continue with Google
</button>
<button
onClick={() => signIn.social({ provider: 'github' })}
className="btn btn-outline"
>
Continue with GitHub
</button>
</div>
);
}
API Route Authentication
Protected API Routes
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';
export async function GET(request: NextRequest) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Access user info
const userId = session.user.id;
// ... rest of handler
}
Helper for API Routes
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
export async function withAuth<T>(
handler: (session: Session) => Promise<T>
): Promise<NextResponse> {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
try {
const result = await handler(session);
return NextResponse.json(result);
} catch (error) {
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}
Database Schema
Better Auth requires these tables. Add to your Drizzle schema:
import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core';
export const user = pgTable('user', {
id: text('id').primaryKey(),
name: text('name'),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').notNull().default(false),
image: text('image'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const session = pgTable('session', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
token: text('token').notNull().unique(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const account = pgTable('account', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
scope: text('scope'),
idToken: text('id_token'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const verification = pgTable('verification', {
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
Or generate with CLI:
npx @better-auth/cli generate
Troubleshooting
"Invalid session" errors
- Check
BETTER_AUTH_SECRETis set and consistent - Verify
BETTER_AUTH_URLmatches your domain - Ensure cookies are being set (check devtools)
OAuth callback fails
- Verify callback URL in provider dashboard matches your app
- Check client ID/secret are correct
- Ensure
trustedOriginsincludes your domain
Session not persisting
- Check
nextCookies()plugin is added (must be last) - Verify
httpOnlyandsecuresettings for production
Security Checklist
-
BETTER_AUTH_SECRETis random, 32+ characters - OAuth secrets stored in environment variables
-
trustedOriginsis properly configured - HTTPS in production
- Always validate session in API routes (not just proxy)
- Protect sensitive routes in proxy.ts
More from aussiegingersnap/cursor-skills
api-rest
REST API conventions for Next.js App Router with Zod validation and standardized error handling. This skill should be used when creating API routes, implementing CRUD operations, or establishing API patterns for a project.
12ui-design-system
Complete design system with principles + living style guide. Enforces precise, crafted UI inspired by Linear, Notion, and Stripe. Includes boilerplate style-guide page template for Next.js/React projects. Use when building any UI that needs Jony Ive-level precision.
10tools-repo-review
Comprehensive GitHub repository analysis for engineering managers with contribution stats, code quality review, team health metrics, and actionable management outputs
9ui-principles
Enforce a precise, minimal design system inspired by Linear, Notion, and Stripe. Use this skill when building dashboards, admin interfaces, or any UI that needs Jony Ive-level precision - clean, modern, minimalist with taste. Every pixel matters.
8ui-shadcn-studio
shadcn/studio component library with MCP integration, theme generation, and block patterns. This skill should be used when building UI with shadcn components, selecting dashboard layouts, or generating landing pages. Canonical source for all shadcn-based work.
7db-postgres
PostgreSQL database management with Drizzle ORM, versioned migrations, and type-safe queries. This skill should be used when setting up a new database, writing migrations, managing schemas, or troubleshooting database issues in PostgreSQL projects.
7