Skills
better-auth-patterns
better-auth-patterns
npx @loomcraft/cli add skill better-auth-patternsFrontmatter
Name
better-auth-patterns
Description
Better Auth setup with session management, social login, organization plugin, and middleware. Use when implementing authentication, adding social login providers, or managing user sessions with Better Auth.
Content
# Better Auth Patterns
## Critical Rules
- **Server-side session check** — use `getCurrentUser()` in RSC and Server Actions.
- **Client-side `useSession()`** — only in Client Components for UI state.
- **Middleware for redirects** — protect routes at the edge, not in pages.
- **Never expose auth secrets** — keep `BETTER_AUTH_SECRET` server-only.
- **Use plugins** — `organization()`, `admin()` for built-in features.
- **Social login always available** — Google + GitHub as defaults.
## Server Setup
```ts
// src/lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { organization, admin } from "better-auth/plugins";
import { db } from "@/lib/db";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg" }),
emailAndPassword: { enabled: true },
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!,
},
},
plugins: [
organization(),
admin(),
],
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // refresh daily
},
});
```
## Client Setup
```ts
// src/lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { organizationClient, adminClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [organizationClient(), adminClient()],
});
export const { useSession, signIn, signUp, signOut } = authClient;
```
## API Route Handler
```ts
// 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);
```
## Session Management
### Server-Side Session
```ts
// src/lib/auth-server.ts
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export async function getCurrentUser() {
const session = await auth.api.getSession({
headers: await headers(),
});
return session?.user ?? null;
}
export async function requireAuth() {
const user = await getCurrentUser();
if (!user) redirect("/login");
return user;
}
```
### Client-Side Session
```tsx
"use client";
import { useSession } from "@/lib/auth-client";
export function UserMenu() {
const { data: session, isPending } = useSession();
if (isPending) return <Skeleton />;
if (!session) return <LoginButton />;
return <span>{session.user.name}</span>;
}
```
## Authentication Flows
### Sign Up
```tsx
"use client";
import { authClient } from "@/lib/auth-client";
async function handleSignUp(data: SignUpInput) {
const { error } = await authClient.signUp.email({
email: data.email,
password: data.password,
name: data.name,
});
if (error) toast.error(error.message);
else router.push("/dashboard");
}
```
### Social Login
```tsx
async function handleGoogleLogin() {
await authClient.signIn.social({ provider: "google" });
}
```
### Sign Out
```tsx
async function handleSignOut() {
await authClient.signOut();
router.push("/");
}
```
## Middleware
```ts
// middleware.ts
import { betterFetch } from "@better-fetch/fetch";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import type { Session } from "better-auth/types";
const publicRoutes = ["/", "/login", "/register", "/api/auth"];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip public routes
if (publicRoutes.some((r) => pathname.startsWith(r))) {
return NextResponse.next();
}
// Check session
const { data: session } = await betterFetch<Session>(
"/api/auth/get-session",
{
baseURL: request.nextUrl.origin,
headers: { cookie: request.headers.get("cookie") ?? "" },
}
);
if (!session) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Admin route protection
if (pathname.startsWith("/admin") && session.user.role !== "admin") {
return NextResponse.redirect(new URL("/", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
```
## Organization Plugin
```tsx
// Create organization
const { data: org } = await authClient.organization.create({
name: "Acme Inc",
slug: "acme",
});
// Invite member
await authClient.organization.inviteMember({
email: "user@example.com",
role: "member",
organizationId: org.id,
});
// Switch active organization
await authClient.organization.setActive({ organizationId: org.id });
```
## Do
- Use `getCurrentUser()` in Server Components and Server Actions for session checks.
- Keep auth client and server modules in separate files (`auth-client.ts` vs `auth.ts`).
- Configure session refresh (`updateAge`) to avoid unnecessary database lookups.
- Use the organization plugin for multi-tenant features instead of building custom logic.
- Always redirect unauthenticated users from middleware, not from page components.
## Don't
- Don't call `useSession()` in Server Components — it is a client-only hook.
- Don't store `BETTER_AUTH_SECRET` in `NEXT_PUBLIC_` variables or expose it to the browser.
- Don't skip the middleware and rely solely on page-level auth checks — middleware catches unauthenticated requests earlier.
- Don't create custom session tables — use Better Auth's built-in schema and plugins.
## Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| **Checking auth in every page** | Duplicated logic, easy to forget on new pages | Use middleware for route protection and `requireAuth()` in server components |
| **Using `process.env.BETTER_AUTH_SECRET` on the client** | Exposes secret, compromises all sessions | Keep secret server-only; use `auth-client.ts` for client code |
| **Fetching session in `useEffect`** | Causes flash of unauthenticated content, race conditions | Use `useSession()` hook which manages loading state automatically |
| **Hardcoding social provider config** | Breaks when credentials rotate or new providers are added | Store client IDs/secrets in env vars, validate with Zod at startup |
| **Not setting `expiresIn` and `updateAge`** | Sessions never expire or refresh too often | Configure both values explicitly in the `session` config |Files
No additional files