Skip to content
Back to skills

Better Auth

ASecurity

Stack: `better-auth` + Drizzle adapter + Next.js App Router **NOT NextAuth. NOT Clerk.** Better Auth is the auth library for all projects.

  • 6 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 27, 2026
developmentrustgobashreactnextjsgitapidatabase

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned May 27, 2026

npx -y skills add KevinZai/commander --skill better-auth --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Better Auth?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Better Auth
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/kevinzai-better-auth/badge)](https://www.skillsdirectory.com/skills/kevinzai-better-auth)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# Better Auth Skill

Stack: `better-auth` + Drizzle adapter + Next.js App Router
**NOT NextAuth. NOT Clerk.** Better Auth is the auth library for all projects.

## Install

```bash
npm install better-auth
npm install better-auth/cli # for schema generation
```

---

## Server Config (`src/lib/auth.ts`)

```ts
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { organization, twoFactor, admin, magicLink } from 'better-auth/plugins';
import { db } from '@/db';
import * as schema from '@/db/schema';

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: 'pg',
    schema: {
      user: schema.users,
      session: schema.sessions,
      account: schema.accounts,
      verification: schema.verifications,
      // organization plugin tables
      organization: schema.organizations,
      member: schema.members,
      invitation: schema.invitations,
    },
  }),
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
    sendVerificationEmail: async ({ user, url }) => {
      await sendEmail({ to: user.email, subject: 'Verify your email', html: `<a href="${url}">Verify</a>` });
    },
    sendResetPasswordEmail: async ({ user, url }) => {
      await sendEmail({ to: user.email, subject: 'Reset your password', html: `<a href="${url}">Reset</a>` });
    },
  },
  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({
      allowUserToCreateOrganization: true,
      creatorRole: 'owner',
      sendInvitationEmail: async ({ invitation, url }) => {
        await sendEmail({
          to: invitation.email,
          subject: `You've been invited to join ${invitation.organization.name}`,
          html: `<a href="${url}">Accept Invitation</a>`,
        });
      },
    }),
    twoFactor({
      issuer: 'MyApp',
      totpOptions: { digits: 6, period: 30 },
    }),
    admin(),
    magicLink({
      sendMagicLink: async ({ email, url }) => {
        await sendEmail({ to: email, subject: 'Your magic link', html: `<a href="${url}">Sign in</a>` });
      },
    }),
  ],
  session: {
    expiresIn: 60 * 60 * 24 * 30, // 30 days
    updateAge: 60 * 60 * 24,       // refresh if >1 day old
    cookieCache: {
      enabled: true,
      maxAge: 60 * 5, // 5 min client-side cache
    },
  },
  trustedOrigins: [process.env.NEXT_PUBLIC_APP_URL!],
});

export type Auth = typeof auth;
```

---

## Client Config (`src/lib/auth-client.ts`)

```ts
import { createAuthClient } from 'better-auth/react';
import { organizationClient, twoFactorClient, adminClient, magicLinkClient } from 'better-auth/client/plugins';

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL!,
  plugins: [
    organizationClient(),
    twoFactorClient(),
    adminClient(),
    magicLinkClient(),
  ],
});

export const {
  signIn,
  signOut,
  signUp,
  useSession,
  organization,
} = authClient;
```

---

## API Route (`src/app/api/auth/[...all]/route.ts`)

```ts
import { auth } from '@/lib/auth';
import { toNextJsHandler } from 'better-auth/next-js';

export const { GET, POST } = toNextJsHandler(auth.handler);
```

---

## Database Schema (required tables)

Run Better Auth CLI to generate:
```bash
npx better-auth generate --output src/db/auth-schema.ts
```

Or use the schema from `drizzle-neon` skill → `schema-patterns.md` (users, sessions, accounts, verifications, organizations, members, invitations).

Custom fields on user:
```ts
export const users = pgTable('users', {
  // ... required Better Auth fields ...
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  emailVerified: boolean('email_verified').default(false).notNull(),
  image: text('image'),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
  // Custom fields
  role: text('role').default('user'), // app-level role
  onboardedAt: timestamp('onboarded_at'),
  stripeCustomerId: text('stripe_customer_id'),
});
```

Tell Better Auth about custom fields:
```ts
// In auth.ts
export const auth = betterAuth({
  user: {
    additionalFields: {
      role: { type: 'string', defaultValue: 'user' },
      stripeCustomerId: { type: 'string', required: false },
    },
  },
  // ...
});
```

---

## Email/Password

```ts
// Sign up
await authClient.signUp.email({
  email: 'user@example.com',
  password: 'SecurePass123!',
  name: 'John Doe',
  callbackURL: '/dashboard',
});

// Sign in
await authClient.signIn.email({
  email: 'user@example.com',
  password: 'SecurePass123!',
  callbackURL: '/dashboard',
  rememberMe: true,
});

// Sign out
await authClient.signOut();

// Request password reset
await authClient.forgetPassword({
  email: 'user@example.com',
  redirectTo: '/auth/reset-password',
});

// Reset password (from reset link)
await authClient.resetPassword({
  newPassword: 'NewPass456!',
});

// Verify email resend
await authClient.sendVerificationEmail({
  email: 'user@example.com',
  callbackURL: '/dashboard',
});
```

---

## Social OAuth

```ts
// Google sign in
await authClient.signIn.social({
  provider: 'google',
  callbackURL: '/dashboard',
});

// GitHub sign in
await authClient.signIn.social({
  provider: 'github',
  callbackURL: '/dashboard',
  scopes: ['user:email', 'read:org'],
});
```

---

## Session Management

```tsx
// Client component — useSession hook
'use client';
import { useSession } from '@/lib/auth-client';

export function UserMenu() {
  const { data: session, isPending, error } = useSession();

  if (isPending) return <Spinner />;
  if (!session) return <SignInButton />;

  return <div>Hello {session.user.name}</div>;
}
```

```ts
// Server component / Server action — getSession
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';

export async function getServerSession() {
  return auth.api.getSession({ headers: await headers() });
}

// In Server Component
export default async function Dashboard() {
  const session = await getServerSession();
  if (!session) redirect('/login');
  return <div>Welcome {session.user.name}</div>;
}

// In Server Action
'use server';
export async function updateProfile(data: FormData) {
  const session = await getServerSession();
  if (!session) throw new Error('Unauthorized');
  // ... do work
}
```

---

## Middleware (`src/middleware.ts`)

```ts
import { NextRequest, NextResponse } from 'next/server';
import { getSessionCookie } from 'better-auth/cookies';

const PUBLIC_ROUTES = ['/', '/login', '/signup', '/api/auth'];
const AUTH_ROUTES = ['/login', '/signup'];

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Allow public routes and API auth routes
  if (PUBLIC_ROUTES.some(r => pathname.startsWith(r))) {
    return NextResponse.next();
  }

  const session = await getSessionCookie(request);

  if (!session) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('callbackUrl', pathname);
    return NextResponse.redirect(loginUrl);
  }

  // Redirect authenticated users away from auth pages
  if (AUTH_ROUTES.includes(pathname)) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)'],
};
```

---

## Organization (Multi-Tenant)

```ts
// Create org
await authClient.organization.create({
  name: 'Acme Corp',
  slug: 'acme-corp',
});

// Switch active org
await authClient.organization.setActive({ organizationId: org.id });

// Invite member
await authClient.organization.inviteMember({
  email: 'colleague@example.com',
  role: 'member',
  organizationId: org.id,
});

// Accept invitation (from email link)
await authClient.organization.acceptInvitation({ invitationId });

// List org members
const members = await authClient.organization.listMembers({ organizationId: org.id });

// Update member role
await authClient.organization.updateMemberRole({
  memberId: member.id,
  role: 'admin',
  organizationId: org.id,
});

// Server-side: get active org
const session = await auth.api.getSession({ headers: await headers() });
const activeOrgId = session?.session.activeOrganizationId;
```

---

## Two-Factor (TOTP)

```ts
// Enable 2FA
const { data } = await authClient.twoFactor.enable({ password: 'current-password' });
// data.totpURI — show as QR code
// data.backupCodes — store securely

// Verify TOTP code
await authClient.twoFactor.verifyTotp({ code: '123456' });

// Disable 2FA
await authClient.twoFactor.disable({ password: 'current-password' });

// During sign in (if 2FA required)
const result = await authClient.signIn.email({ email, password });
if (result.data?.twoFactorRedirect) {
  // Show TOTP input
  await authClient.twoFactor.verifyTotp({ code: totpInput });
}
```

---

## Admin Plugin

```ts
// Server-side admin operations
const users = await auth.api.listUsers({
  headers: await headers(),
  query: { limit: 20, offset: 0 },
});

await auth.api.banUser({ userId, reason: 'Violation' });
await auth.api.unbanUser({ userId });
await auth.api.impersonateUser({ userId }); // get session as that user
await auth.api.revokeUserSessions({ userId });
```

---

## Magic Link

```ts
await authClient.signIn.magicLink({
  email: 'user@example.com',
  callbackURL: '/dashboard',
});
```

---

## Role-Based Access (Server)

```ts
import { auth } from '@/lib/auth';

async function requireRole(role: 'admin' | 'owner') {
  const session = await auth.api.getSession({ headers: await headers() });
  if (!session) throw new Error('Unauthorized');
  if (session.user.role !== role) throw new Error('Forbidden');
  return session;
}

// In a Server Action
export async function deleteOrganization(orgId: string) {
  const session = await requireRole('admin');
  await db.delete(organizations).where(eq(organizations.id, orgId));
}
```

Files in this skill

  • SKILL.md10 KB
  • references/setup-checklist.md3.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.