xiaohongshu
xiaohongshu
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.
Full skill instructions
Integration patterns for connecting to Better Auth in Next.js 16 projects using the Drizzle adapter.
Better Auth is a TypeScript-first authentication framework that handles:
This skill covers connecting to Better Auth, not building the auth service itself.
npm install better-auth
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
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;
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;
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 callbacksCreate 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).*)'],
};
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.
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>
);
}
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} />;
}
'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>
);
}
'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>
);
}
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
}
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 }
);
}
}
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
BETTER_AUTH_SECRET is set and consistentBETTER_AUTH_URL matches your domaintrustedOrigins includes your domainnextCookies() plugin is added (must be last)httpOnly and secure settings for productionBETTER_AUTH_SECRET is random, 32+ characterstrustedOrigins is properly configuredxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
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...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
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...
Display project status, active tracks, and next actions
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.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience