Skip to content
auth-better-auth logo

Better Auth Skill

auth-better-auth

Better Auth integration for Next.js 16 with Drizzle adapter. This skill should be used when connecting to a Better Auth instance, configuring OAuth providers, or implementing protected routes with proxy.ts.

SKILL.md

Full skill instructions

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_SECRET is set and consistent
  • Verify BETTER_AUTH_URL matches 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 trustedOrigins includes your domain

Session not persisting

  • Check nextCookies() plugin is added (must be last)
  • Verify httpOnly and secure settings for production

Security Checklist

  • BETTER_AUTH_SECRET is random, 32+ characters
  • OAuth secrets stored in environment variables
  • trustedOrigins is properly configured
  • HTTPS in production
  • Always validate session in API routes (not just proxy)
  • Protect sensitive routes in proxy.ts

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View