agentsclimarketplace

Better auth patterns

Skill smicolon/ai-kit/packs/better-auth/skills/better-auth-patterns

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

Install
npx -y skills add smicolon/ai-kit --skill better-auth-patterns

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

9.9 KB, ~2.4k tokens by cl100k_base, as published. Nobody here has run it

Better Auth Patterns

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

Server Configuration

Basic Setup

// 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

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

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)

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

// 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

// 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

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

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

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

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

// 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

// 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

// 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

// ❌ 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) { ... }

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.