Skip to content
Back to skills

Better Auth Patterns

ASecurity

Auto-enforce Better Auth implementation patterns. Activates when setting up authentication, configuring providers, creating auth forms, or implementing session management in React applications.

  • 6 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 28, 2026
ai-agentstypescriptgosqlreactgitapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Scanned May 28, 2026

npx -y skills add smicolon/ai-kit --skill better-auth-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Better Auth Patterns?

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

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

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
---
name: Better Auth Patterns
description: >-
  Auto-enforce Better Auth implementation patterns. Activates when setting up
  authentication, configuring providers, creating auth forms, or implementing
  session management in React applications.
version: 1.0.0
---

# Better Auth Patterns

This skill enforces Better Auth best practices for authentication in React applications.

## Server Configuration

### Basic Setup
```typescript
// lib/auth.ts
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import { prisma } from './prisma'

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: 'postgresql',
  }),

  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },

  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24,     // Extend daily
  },
})

export type Auth = typeof auth
```

### Social Providers
```typescript
import { betterAuth } from 'better-auth'

export const auth = betterAuth({
  // ... database config

  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      scopes: ['email', 'profile'],
    },
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
      scopes: ['user:email'],
    },
    discord: {
      clientId: process.env.DISCORD_CLIENT_ID!,
      clientSecret: process.env.DISCORD_CLIENT_SECRET!,
    },
  },
})
```

### Two-Factor Authentication
```typescript
import { twoFactor } from 'better-auth/plugins/two-factor'

export const auth = betterAuth({
  // ... base config

  plugins: [
    twoFactor({
      issuer: 'MyApp',
      totpWindow: 1,
      backupCodes: {
        enabled: true,
        count: 10,
      },
    }),
  ],
})
```

### Passkeys (WebAuthn)
```typescript
import { passkey } from 'better-auth/plugins/passkey'

export const auth = betterAuth({
  // ... base config

  plugins: [
    passkey({
      rpId: 'myapp.com',
      rpName: 'My App',
      origin: 'https://myapp.com',
      attestation: 'none',
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        userVerification: 'preferred',
        residentKey: 'preferred',
      },
    }),
  ],
})
```

## Client Setup

### Create Auth Client
```typescript
// auth/client.ts
import { createAuthClient } from 'better-auth/react'
import type { Auth } from '@/lib/auth'

export const authClient = createAuthClient<Auth>({
  baseURL: import.meta.env.VITE_API_URL,
  // Optional: Custom fetch for headers
  fetch: async (url, options) => {
    return fetch(url, {
      ...options,
      credentials: 'include',
    })
  },
})

// Export typed methods
export const {
  signIn,
  signUp,
  signOut,
  useSession,
  getSession,
  resetPassword,
  verifyEmail,
  // Social
  signInWithGoogle,
  signInWithGithub,
  // 2FA
  enable2FA,
  disable2FA,
  verify2FA,
  // Passkeys
  registerPasskey,
  signInWithPasskey,
} = authClient
```

### Session Hook
```typescript
// auth/hooks.ts
import { useSession } from './client'

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

  return {
    user: session?.user ?? null,
    session: session?.session ?? null,
    isAuthenticated: !!session?.user,
    isLoading: isPending,
    error,
  }
}

export function useRequireAuth() {
  const auth = useAuth()
  const navigate = useNavigate()

  useEffect(() => {
    if (!auth.isLoading && !auth.isAuthenticated) {
      navigate({ to: '/login' })
    }
  }, [auth.isLoading, auth.isAuthenticated])

  return auth
}
```

## Auth Components

### Login Form
```typescript
import { useForm } from '@tanstack/react-form'
import { zodValidator } from '@tanstack/zod-form-adapter'
import { signIn } from '@/auth/client'
import { z } from 'zod'

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(1, 'Password is required'),
})

export function LoginForm() {
  const [error, setError] = useState<string | null>(null)
  const navigate = useNavigate()

  const form = useForm({
    defaultValues: { email: '', password: '' },
    validatorAdapter: zodValidator(),
    validators: { onChange: loginSchema },
    onSubmit: async ({ value }) => {
      setError(null)
      const result = await signIn.email(value)

      if (result.error) {
        setError(result.error.message)
        return
      }

      navigate({ to: '/dashboard' })
    },
  })

  return (
    <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit() }}>
      {error && <div className="error">{error}</div>}

      <form.Field
        name="email"
        children={(field) => (/* email input */)}
      />

      <form.Field
        name="password"
        children={(field) => (/* password input */)}
      />

      <form.Subscribe
        selector={(state) => state.isSubmitting}
        children={(isSubmitting) => (
          <button type="submit" disabled={isSubmitting}>
            {isSubmitting ? 'Signing in...' : 'Sign In'}
          </button>
        )}
      />
    </form>
  )
}
```

### Social Login Buttons
```typescript
import { signInWithGoogle, signInWithGithub } from '@/auth/client'

export function SocialLoginButtons() {
  const handleGoogleLogin = async () => {
    await signInWithGoogle({
      callbackURL: '/dashboard',
    })
  }

  const handleGithubLogin = async () => {
    await signInWithGithub({
      callbackURL: '/dashboard',
    })
  }

  return (
    <div className="flex flex-col gap-2">
      <button onClick={handleGoogleLogin} className="btn-social">
        <GoogleIcon /> Continue with Google
      </button>
      <button onClick={handleGithubLogin} className="btn-social">
        <GithubIcon /> Continue with GitHub
      </button>
    </div>
  )
}
```

### 2FA Setup
```typescript
import { enable2FA, verify2FA } from '@/auth/client'
import { useState } from 'react'

export function TwoFactorSetup() {
  const [qrCode, setQrCode] = useState<string | null>(null)
  const [secret, setSecret] = useState<string | null>(null)
  const [code, setCode] = useState('')
  const [backupCodes, setBackupCodes] = useState<string[]>([])

  const handleEnable = async () => {
    const result = await enable2FA()
    if (result.data) {
      setQrCode(result.data.qrCode)
      setSecret(result.data.secret)
    }
  }

  const handleVerify = async () => {
    const result = await verify2FA({ code })
    if (result.data) {
      setBackupCodes(result.data.backupCodes)
    }
  }

  if (backupCodes.length > 0) {
    return (
      <div>
        <h3>Save your backup codes</h3>
        <ul>
          {backupCodes.map((code, i) => (
            <li key={i}>{code}</li>
          ))}
        </ul>
      </div>
    )
  }

  if (qrCode) {
    return (
      <div>
        <img src={qrCode} alt="2FA QR Code" />
        <p>Secret: {secret}</p>
        <input
          value={code}
          onChange={(e) => setCode(e.target.value)}
          placeholder="Enter 6-digit code"
        />
        <button onClick={handleVerify}>Verify</button>
      </div>
    )
  }

  return (
    <button onClick={handleEnable}>Enable 2FA</button>
  )
}
```

### Passkey Registration
```typescript
import { registerPasskey, signInWithPasskey } from '@/auth/client'

export function PasskeyManager() {
  const [error, setError] = useState<string | null>(null)

  const handleRegister = async () => {
    setError(null)
    const result = await registerPasskey({
      name: 'My Device',
    })

    if (result.error) {
      setError(result.error.message)
    }
  }

  const handleSignIn = async () => {
    setError(null)
    const result = await signInWithPasskey()

    if (result.error) {
      setError(result.error.message)
    }
  }

  return (
    <div>
      {error && <div className="error">{error}</div>}
      <button onClick={handleRegister}>Register Passkey</button>
      <button onClick={handleSignIn}>Sign in with Passkey</button>
    </div>
  )
}
```

## Router Integration

### Root Route with Session
```typescript
// routes/__root.tsx
import { createRootRouteWithContext } from '@tanstack/react-router'
import { getSession } from '@/auth/client'

export const Route = createRootRouteWithContext<RouterContext>()({
  beforeLoad: async () => {
    const session = await getSession()
    return { session: session?.data ?? null }
  },
})
```

### Protected Route Layout
```typescript
// routes/_auth.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'

export const Route = createFileRoute('/_auth')({
  beforeLoad: async ({ context, location }) => {
    if (!context.session) {
      throw redirect({
        to: '/login',
        search: { redirect: location.pathname },
      })
    }
  },
})
```

### Guest-Only Routes
```typescript
// routes/_guest.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'

export const Route = createFileRoute('/_guest')({
  beforeLoad: async ({ context }) => {
    if (context.session) {
      throw redirect({ to: '/dashboard' })
    }
  },
})
```

## Conventions

1. **Server config in lib/** - Keep auth config at `lib/auth.ts`
2. **Client in auth/** - Export typed client from `auth/client.ts`
3. **Route protection** - Use pathless layouts (`_auth.tsx`)
4. **Session in context** - Load session in `__root.tsx`
5. **Error handling** - Always check `result.error`
6. **Type safety** - Use `Auth` type from server config

## Anti-Patterns

```typescript
// ❌ WRONG: Untyped client
const authClient = createAuthClient({})

// ✅ CORRECT: Typed client
const authClient = createAuthClient<Auth>({})

// ❌ WRONG: No error handling
const result = await signIn.email(data)
navigate('/dashboard')

// ✅ CORRECT: Check for errors
const result = await signIn.email(data)
if (result.error) {
  setError(result.error.message)
  return
}
navigate('/dashboard')

// ❌ WRONG: Client-side session check only
if (localStorage.getItem('session')) { ... }

// ✅ CORRECT: Server-validated session
const { data: session } = useSession()
if (session) { ... }
```

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.