Frontend Better Auth TypeScript Skill
Overview
Specialized skill for implementing Better Auth on the frontend with TypeScript. Better Auth is a modern, framework-agnostic authentication library that provides type-safe authentication with minimal configuration.
Core Capabilities
1. Better Auth Client Setup
- •Configure type-safe auth client
- •Initialize Better Auth with proper TypeScript types
- •Set up authentication providers (OAuth, Email, etc.)
- •Configure session management
- •Handle client-side routing protection
2. Authentication Flows
- •Email/password authentication
- •OAuth providers (Google, GitHub, Discord, etc.)
- •Magic link authentication
- •Two-factor authentication (2FA)
- •Email verification
- •Password reset flows
3. Session Management
- •Access user session data with type safety
- •Handle session refresh automatically
- •Implement session persistence
- •Handle session expiration
- •Clear sessions on logout
4. Protected Routes & Components
- •Create route guards for protected pages
- •Implement loading states during auth checks
- •Handle unauthorized access
- •Redirect logic after authentication
- •Role-based UI rendering
Installation & Setup
Install Better Auth
bash
npm install better-auth # or pnpm add better-auth # or yarn add better-auth
Basic Client Configuration
typescript
// lib/auth-client.ts
import { createAuthClient } from "better-auth/client"
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000",
// Optional: customize session storage
storage: {
type: "cookie", // or "localStorage"
cookieOptions: {
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
},
},
})
// Export typed auth methods
export const {
signIn,
signUp,
signOut,
useSession,
getSession,
} = authClient
Framework-Specific Implementations
React / Next.js
Auth Provider Setup
typescript
// components/auth-provider.tsx
"use client"
import { SessionProvider } from "better-auth/react"
import { authClient } from "@/lib/auth-client"
export function AuthProvider({ children }: { children: React.ReactNode }) {
return (
<SessionProvider client={authClient}>
{children}
</SessionProvider>
)
}
Using Session in Components
typescript
// components/user-menu.tsx
"use client"
import { useSession } from "@/lib/auth-client"
import { signOut } from "@/lib/auth-client"
export function UserMenu() {
const { data: session, isPending } = useSession()
if (isPending) {
return <div>Loading...</div>
}
if (!session) {
return <a href="/login">Sign In</a>
}
return (
<div>
<p>Welcome, {session.user.name}</p>
<button onClick={() => signOut()}>
Sign Out
</button>
</div>
)
}
Protected Route Component
typescript
// components/protected-route.tsx
"use client"
import { useSession } from "@/lib/auth-client"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { data: session, isPending } = useSession()
const router = useRouter()
useEffect(() => {
if (!isPending && !session) {
router.push("/login")
}
}, [session, isPending, router])
if (isPending) {
return <div>Loading...</div>
}
if (!session) {
return null
}
return <>{children}</>
}
Login Form Component
typescript
// components/login-form.tsx
"use client"
import { useState } from "react"
import { signIn } from "@/lib/auth-client"
import { useRouter } from "next/navigation"
export function LoginForm() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const router = useRouter()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
setLoading(true)
try {
const result = await signIn.email({
email,
password,
})
if (result.error) {
setError(result.error.message)
} else {
router.push("/dashboard")
}
} catch (err) {
setError("An unexpected error occurred")
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit}>
{error && <div className="error">{error}</div>}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit" disabled={loading}>
{loading ? "Signing in..." : "Sign In"}
</button>
</form>
)
}
OAuth Sign In
typescript
// components/oauth-buttons.tsx
"use client"
import { signIn } from "@/lib/auth-client"
export function OAuthButtons() {
const handleOAuthSignIn = async (provider: "google" | "github") => {
await signIn.social({
provider,
callbackURL: "/dashboard",
})
}
return (
<div>
<button onClick={() => handleOAuthSignIn("google")}>
Sign in with Google
</button>
<button onClick={() => handleOAuthSignIn("github")}>
Sign in with GitHub
</button>
</div>
)
}
Vue.js
Setup Composable
typescript
// composables/useAuth.ts
import { ref, computed } from "vue"
import { authClient } from "@/lib/auth-client"
export function useAuth() {
const session = ref(null)
const loading = ref(true)
const isAuthenticated = computed(() => !!session.value)
const loadSession = async () => {
try {
session.value = await authClient.getSession()
} finally {
loading.value = false
}
}
const signIn = async (email: string, password: string) => {
const result = await authClient.signIn.email({ email, password })
if (!result.error) {
session.value = result.data
}
return result
}
const signOut = async () => {
await authClient.signOut()
session.value = null
}
return {
session,
loading,
isAuthenticated,
loadSession,
signIn,
signOut,
}
}
Svelte
Auth Store
typescript
// stores/auth.ts
import { writable } from "svelte/store"
import { authClient } from "@/lib/auth-client"
function createAuthStore() {
const { subscribe, set, update } = writable({
session: null,
loading: true,
})
return {
subscribe,
loadSession: async () => {
const session = await authClient.getSession()
set({ session, loading: false })
},
signIn: async (email: string, password: string) => {
const result = await authClient.signIn.email({ email, password })
if (!result.error) {
update(state => ({ ...state, session: result.data }))
}
return result
},
signOut: async () => {
await authClient.signOut()
set({ session: null, loading: false })
},
}
}
export const auth = createAuthStore()
Advanced Patterns
Role-Based Access Control
typescript
// hooks/useAuthorization.ts
import { useSession } from "@/lib/auth-client"
type Role = "admin" | "user" | "moderator"
export function useAuthorization() {
const { data: session } = useSession()
const hasRole = (role: Role | Role[]) => {
if (!session?.user) return false
const userRoles = session.user.roles || []
const rolesToCheck = Array.isArray(role) ? role : [role]
return rolesToCheck.some(r => userRoles.includes(r))
}
const hasPermission = (permission: string) => {
if (!session?.user) return false
const userPermissions = session.user.permissions || []
return userPermissions.includes(permission)
}
return {
hasRole,
hasPermission,
isAdmin: hasRole("admin"),
isModerator: hasRole("moderator"),
}
}
Conditional Rendering Component
typescript
// components/can.tsx
import { useAuthorization } from "@/hooks/useAuthorization"
interface CanProps {
role?: string | string[]
permission?: string
fallback?: React.ReactNode
children: React.ReactNode
}
export function Can({ role, permission, fallback = null, children }: CanProps) {
const { hasRole, hasPermission } = useAuthorization()
const isAuthorized = role
? hasRole(role as any)
: permission
? hasPermission(permission)
: false
return isAuthorized ? <>{children}</> : <>{fallback}</>
}
// Usage:
// <Can role="admin">
// <AdminPanel />
// </Can>
Server-Side Session Validation (Next.js)
typescript
// lib/auth-server.ts
import { cookies } from "next/headers"
import { authClient } from "./auth-client"
export async function getServerSession() {
const cookieStore = cookies()
const sessionToken = cookieStore.get("session")?.value
if (!sessionToken) {
return null
}
try {
const session = await authClient.getSession()
return session
} catch {
return null
}
}
// Usage in Server Components:
// const session = await getServerSession()
Middleware for Protected Routes (Next.js)
typescript
// middleware.ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export async function middleware(request: NextRequest) {
const session = request.cookies.get("session")
// Protected routes
if (request.nextUrl.pathname.startsWith("/dashboard")) {
if (!session) {
return NextResponse.redirect(new URL("/login", request.url))
}
}
// Auth routes (redirect if logged in)
if (request.nextUrl.pathname.startsWith("/login")) {
if (session) {
return NextResponse.redirect(new URL("/dashboard", request.url))
}
}
return NextResponse.next()
}
export const config = {
matcher: ["/dashboard/:path*", "/login", "/signup"],
}
TypeScript Types & Interfaces
Extend Session Types
typescript
// types/auth.ts
import "better-auth"
declare module "better-auth" {
interface Session {
user: {
id: string
email: string
name: string
image?: string
emailVerified: boolean
roles?: string[]
permissions?: string[]
}
}
}
Best Practices
1. Error Handling
typescript
const handleAuth = async () => {
try {
const result = await signIn.email({ email, password })
if (result.error) {
// Handle specific errors
switch (result.error.code) {
case "INVALID_CREDENTIALS":
setError("Invalid email or password")
break
case "EMAIL_NOT_VERIFIED":
setError("Please verify your email first")
break
default:
setError("Authentication failed")
}
}
} catch (err) {
setError("An unexpected error occurred")
console.error(err)
}
}
2. Loading States
typescript
function AuthButton() {
const { data: session, isPending } = useSession()
const [isLoading, setIsLoading] = useState(false)
if (isPending) {
return <Spinner />
}
return session ? (
<button
onClick={() => {
setIsLoading(true)
signOut().finally(() => setIsLoading(false))
}}
disabled={isLoading}
>
{isLoading ? "Signing out..." : "Sign Out"}
</button>
) : (
<a href="/login">Sign In</a>
)
}
3. Optimistic Updates
typescript
const handleSignOut = async () => {
// Optimistically update UI
queryClient.setQueryData(["session"], null)
try {
await signOut()
router.push("/")
} catch (error) {
// Rollback on error
queryClient.invalidateQueries(["session"])
}
}
4. Persist Redirect Path
typescript
// Store intended destination before redirecting to login
const login = () => {
const returnUrl = window.location.pathname
sessionStorage.setItem("returnUrl", returnUrl)
router.push(`/login?returnUrl=${encodeURIComponent(returnUrl)}`)
}
// After successful login
const returnUrl = sessionStorage.getItem("returnUrl") || "/dashboard"
sessionStorage.removeItem("returnUrl")
router.push(returnUrl)
Security Considerations
- •CSRF Protection: Better Auth handles CSRF tokens automatically
- •Secure Cookies: Always use
secure: truein production - •XSS Prevention: Never store sensitive tokens in localStorage
- •Session Timeout: Implement automatic session refresh
- •Rate Limiting: Implement on login/signup endpoints
Testing
Mock Auth for Tests
typescript
// __mocks__/auth-client.ts
export const mockSession = {
user: {
id: "test-user-id",
email: "test@example.com",
name: "Test User",
},
}
export const useSession = jest.fn(() => ({
data: mockSession,
isPending: false,
}))
export const signIn = {
email: jest.fn(),
social: jest.fn(),
}
export const signOut = jest.fn()
When to use this skill:
- •Implementing authentication in React/Next.js/Vue/Svelte apps
- •Building type-safe authentication flows
- •Creating protected routes and components
- •Integrating OAuth providers
- •Managing user sessions on the frontend
- •Implementing role-based access control
- •Building authentication UI components