--- name: clerk description: Auto-activates when user mentions Clerk, authentication, user management, or auth flows. Expert in Clerk authentication including Next.js integration, user management, and session handling. category: auth --- # Clerk Authentication Skill Comprehensive guide for implementing Clerk authentication and user management in Next.js applications with App Router support. ## 1. Setup & Configuration ### Installation ```bash # Install Clerk for Next.js bun add @clerk/nextjs # Install themes (optional) bun add @clerk/themes ``` ### Environment Variables Create `.env.local` with your Clerk keys: ```bash # Required - Get from Clerk Dashboard NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_... # Optional - Custom redirect URLs NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding ``` ### Root Layout Setup (App Router) ✅ **Good: Proper ClerkProvider setup** ```typescript // app/layout.tsx import { ClerkProvider } from '@clerk/nextjs'; import type { Metadata } from 'next'; export const metadata: Metadata = { title: 'My App', description: 'Secure app with Clerk', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` ❌ **Bad: Missing ClerkProvider or incorrect placement** ```typescript // app/layout.tsx export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {/* Missing ClerkProvider - auth won't work */} {children} ); } ``` ### ClerkProvider with Localization ✅ **Good: Custom localization** ```typescript // app/layout.tsx import { ClerkProvider } from '@clerk/nextjs'; import { frFR } from '@clerk/localizations'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` ### Middleware Configuration Create `middleware.ts` in project root: ✅ **Good: Basic middleware setup** ```typescript // middleware.ts import { clerkMiddleware } from '@clerk/nextjs/server'; export default clerkMiddleware(); export const config = { matcher: [ // Skip Next.js internals and static files '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', // Always run for API routes '/(api|trpc)(.*)', ], }; ``` ✅ **Good: Middleware with public routes** ```typescript // middleware.ts import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; const isPublicRoute = createRouteMatcher([ '/', '/sign-in(.*)', '/sign-up(.*)', '/api/webhooks(.*)', ]); export default clerkMiddleware(async (auth, req) => { if (!isPublicRoute(req)) { await auth.protect(); } }); export const config = { matcher: [ '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', '/(api|trpc)(.*)', ], }; ``` ❌ **Bad: Missing matcher config** ```typescript // middleware.ts import { clerkMiddleware } from '@clerk/nextjs/server'; // Missing config.matcher - middleware won't run correctly export default clerkMiddleware(); ``` ### Clerk Dashboard Configuration 1. **Sign-In Settings**: - Email address (required) - Phone number (optional) - Username (optional) 2. **Social Connections**: - Google OAuth - GitHub OAuth - Discord, LinkedIn, etc. 3. **Multi-Factor Authentication**: - SMS code - Authenticator app (TOTP) - Backup codes 4. **Session Configuration**: - Session lifetime: 7 days (default) - Inactive period: 10 minutes (default) --- ## 2. Authentication Methods ### Email/Password Authentication ✅ **Good: Using prebuilt components** ```typescript // app/sign-up/[[...sign-up]]/page.tsx import { SignUp } from '@clerk/nextjs'; export default function SignUpPage() { return (
); } ``` ```typescript // app/sign-in/[[...sign-in]]/page.tsx import { SignIn } from '@clerk/nextjs'; export default function SignInPage() { return (
); } ``` ✅ **Good: Custom email/password flow with Clerk Elements** ```typescript 'use client'; import { useSignUp } from '@clerk/nextjs'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; export default function CustomSignUp() { const { isLoaded, signUp, setActive } = useSignUp(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [pendingVerification, setPendingVerification] = useState(false); const [code, setCode] = useState(''); const router = useRouter(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!isLoaded) return; try { await signUp.create({ emailAddress: email, password, }); // Send verification email await signUp.prepareEmailAddressVerification({ strategy: 'email_code', }); setPendingVerification(true); } catch (err: any) { console.error('Error:', JSON.stringify(err, null, 2)); } }; const handleVerify = async (e: React.FormEvent) => { e.preventDefault(); if (!isLoaded) return; try { const completeSignUp = await signUp.attemptEmailAddressVerification({ code, }); if (completeSignUp.status === 'complete') { await setActive({ session: completeSignUp.createdSessionId }); router.push('/dashboard'); } } catch (err: any) { console.error('Error:', JSON.stringify(err, null, 2)); } }; if (pendingVerification) { return (
setCode(e.target.value)} placeholder="Enter verification code" />
); } return (
setEmail(e.target.value)} type="email" placeholder="Email" /> setPassword(e.target.value)} type="password" placeholder="Password" />
); } ``` ❌ **Bad: Weak password validation** ```typescript // Don't rely on client-side only validation const handleSubmit = async () => { if (password.length < 6) { alert('Password too short'); return; } // Clerk handles strong password validation automatically }; ``` ### OAuth Authentication (Social Login) ✅ **Good: Google OAuth setup** ```typescript 'use client'; import { useSignIn } from '@clerk/nextjs'; export default function OAuthButtons() { const { signIn, isLoaded } = useSignIn(); if (!isLoaded) return null; const signInWithGoogle = () => { signIn.authenticateWithRedirect({ strategy: 'oauth_google', redirectUrl: '/sso-callback', redirectUrlComplete: '/dashboard', }); }; const signInWithGitHub = () => { signIn.authenticateWithRedirect({ strategy: 'oauth_github', redirectUrl: '/sso-callback', redirectUrlComplete: '/dashboard', }); }; return (
); } ``` ✅ **Good: SSO callback handler** ```typescript // app/sso-callback/page.tsx 'use client'; import { AuthenticateWithRedirectCallback } from '@clerk/nextjs'; export default function SSOCallback() { return ; } ``` ✅ **Good: OAuth with additional scopes** ```typescript 'use client'; import { useSignIn } from '@clerk/nextjs'; export default function GoogleCalendarAuth() { const { signIn } = useSignIn(); const signInWithCalendar = () => { signIn?.authenticateWithRedirect({ strategy: 'oauth_google', redirectUrl: '/sso-callback', redirectUrlComplete: '/calendar', additionalScopes: ['https://www.googleapis.com/auth/calendar'], }); }; return ( ); } ``` ### Magic Links (Passwordless) ✅ **Good: Email magic link authentication** ```typescript 'use client'; import { useSignIn } from '@clerk/nextjs'; import { useState } from 'react'; export default function MagicLinkSignIn() { const { signIn, isLoaded } = useSignIn(); const [email, setEmail] = useState(''); const [emailSent, setEmailSent] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!isLoaded) return; try { const result = await signIn.create({ identifier: email, }); const emailLink = result.supportedFirstFactors.find( (factor) => factor.strategy === 'email_link' ); if (emailLink) { await signIn.prepareFirstFactor({ strategy: 'email_link', emailAddressId: emailLink.emailAddressId, redirectUrl: '/verify-magic-link', }); setEmailSent(true); } } catch (err) { console.error('Error:', err); } }; if (emailSent) { return

Check your email for a magic link!

; } return (
setEmail(e.target.value)} placeholder="Enter your email" />
); } ``` ✅ **Good: Magic link verification page** ```typescript // app/verify-magic-link/page.tsx 'use client'; import { useSignIn } from '@clerk/nextjs'; import { useRouter } from 'next/navigation'; import { useEffect } from 'react'; export default function VerifyMagicLink() { const { signIn, setActive } = useSignIn(); const router = useRouter(); useEffect(() => { const verify = async () => { try { const result = await signIn?.attemptFirstFactor({ strategy: 'email_link', }); if (result?.status === 'complete') { await setActive?.({ session: result.createdSessionId }); router.push('/dashboard'); } } catch (err) { console.error('Error verifying:', err); } }; verify(); }, [signIn, setActive, router]); return
Verifying your magic link...
; } ``` ### Phone (SMS) Authentication ✅ **Good: Phone number sign-up** ```typescript 'use client'; import { useSignUp } from '@clerk/nextjs'; import { useState } from 'react'; export default function PhoneSignUp() { const { signUp, isLoaded, setActive } = useSignUp(); const [phone, setPhone] = useState(''); const [code, setCode] = useState(''); const [verifying, setVerifying] = useState(false); const handleSendCode = async (e: React.FormEvent) => { e.preventDefault(); if (!isLoaded) return; try { await signUp.create({ phoneNumber: phone, }); await signUp.preparePhoneNumberVerification(); setVerifying(true); } catch (err) { console.error('Error:', err); } }; const handleVerifyCode = async (e: React.FormEvent) => { e.preventDefault(); if (!isLoaded) return; try { const result = await signUp.attemptPhoneNumberVerification({ code, }); if (result.status === 'complete') { await setActive({ session: result.createdSessionId }); } } catch (err) { console.error('Error:', err); } }; if (verifying) { return (
setCode(e.target.value)} placeholder="Enter SMS code" />
); } return (
setPhone(e.target.value)} placeholder="+1234567890" />
); } ``` ### Multi-Factor Authentication (MFA) ✅ **Good: Enable MFA with TOTP** ```typescript 'use client'; import { useUser } from '@clerk/nextjs'; import { useState } from 'react'; export default function EnableMFA() { const { user } = useUser(); const [qrCode, setQrCode] = useState(''); const [code, setCode] = useState(''); const startMFAEnrollment = async () => { try { const mfaFactor = await user?.createTOTP(); setQrCode(mfaFactor?.uri || ''); } catch (err) { console.error('Error creating TOTP:', err); } }; const verifyMFA = async (e: React.FormEvent) => { e.preventDefault(); try { const totpFactor = user?.totpList?.[0]; await totpFactor?.attemptVerification({ code }); } catch (err) { console.error('Error verifying TOTP:', err); } }; if (qrCode) { return (
QR Code
setCode(e.target.value)} placeholder="Enter code from authenticator app" />
); } return ( ); } ``` ✅ **Good: MFA sign-in flow** ```typescript 'use client'; import { useSignIn } from '@clerk/nextjs'; import { useState } from 'react'; export default function MFASignIn() { const { signIn, isLoaded, setActive } = useSignIn(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [totpCode, setTotpCode] = useState(''); const [needsTOTP, setNeedsTOTP] = useState(false); const handleFirstStep = async (e: React.FormEvent) => { e.preventDefault(); if (!isLoaded) return; try { const result = await signIn.create({ identifier: email, password, }); if (result.status === 'needs_second_factor') { setNeedsTOTP(true); } else if (result.status === 'complete') { await setActive({ session: result.createdSessionId }); } } catch (err) { console.error('Error:', err); } }; const handleTOTPVerify = async (e: React.FormEvent) => { e.preventDefault(); if (!isLoaded) return; try { const result = await signIn.attemptSecondFactor({ strategy: 'totp', code: totpCode, }); if (result.status === 'complete') { await setActive({ session: result.createdSessionId }); } } catch (err) { console.error('Error:', err); } }; if (needsTOTP) { return (
setTotpCode(e.target.value)} placeholder="6-digit code" />
); } return (
setEmail(e.target.value)} type="email" placeholder="Email" /> setPassword(e.target.value)} type="password" placeholder="Password" />
); } ``` ❌ **Bad: Storing MFA backup codes insecurely** ```typescript // Don't store backup codes in localStorage or state const backupCodes = user?.backupCodes; localStorage.setItem('backupCodes', JSON.stringify(backupCodes)); // ❌ Insecure ``` --- ## 3. User Management ### User Object Structure Clerk's user object contains comprehensive user information: ```typescript interface ClerkUser { id: string; firstName: string | null; lastName: string | null; fullName: string | null; username: string | null; primaryEmailAddress: EmailAddress | null; emailAddresses: EmailAddress[]; primaryPhoneNumber: PhoneNumber | null; phoneNumbers: PhoneNumber[]; profileImageUrl: string; imageUrl: string; hasImage: boolean; publicMetadata: Record; privateMetadata: Record; unsafeMetadata: Record; externalAccounts: ExternalAccount[]; createdAt: Date; updatedAt: Date; lastSignInAt: Date | null; } ``` ### Accessing User Data ✅ **Good: Client-side user access** ```typescript 'use client'; import { useUser } from '@clerk/nextjs'; export default function UserProfile() { const { isLoaded, isSignedIn, user } = useUser(); if (!isLoaded) { return
Loading...
; } if (!isSignedIn) { return
Please sign in
; } return (
Profile

{user.fullName}

{user.primaryEmailAddress?.emailAddress}

); } ``` ✅ **Good: Server-side user access** ```typescript // app/dashboard/page.tsx import { auth, currentUser } from '@clerk/nextjs/server'; export default async function DashboardPage() { const { userId } = await auth(); const user = await currentUser(); if (!userId) { return
Not authenticated
; } return (

Welcome, {user?.firstName}!

User ID: {userId}

); } ``` ### Public Metadata vs Private Metadata ✅ **Good: Metadata organization** ```typescript // Public metadata - accessible on frontend, in session token type PublicMetadata = { role: 'admin' | 'user' | 'moderator'; subscriptionTier: 'free' | 'pro' | 'enterprise'; onboardingComplete: boolean; }; // Private metadata - backend only, sensitive data type PrivateMetadata = { stripeCustomerId: string; internalNotes: string; lastPaymentDate: string; }; // Unsafe metadata - read/write on frontend (use sparingly) type UnsafeMetadata = { theme: 'light' | 'dark'; notificationPreferences: { email: boolean; push: boolean; }; }; ``` ✅ **Good: Setting metadata (backend)** ```typescript // app/api/users/[userId]/route.ts import { clerkClient } from '@clerk/nextjs/server'; import { NextRequest, NextResponse } from 'next/server'; export async function PATCH( req: NextRequest, { params }: { params: { userId: string } } ) { const { role } = await req.json(); await clerkClient().users.updateUser(params.userId, { publicMetadata: { role, }, }); return NextResponse.json({ success: true }); } ``` ✅ **Good: Updating unsafe metadata (client-side)** ```typescript 'use client'; import { useUser } from '@clerk/nextjs'; export default function ThemeToggle() { const { user } = useUser(); const toggleTheme = async () => { const currentTheme = user?.unsafeMetadata?.theme || 'light'; const newTheme = currentTheme === 'light' ? 'dark' : 'light'; await user?.update({ unsafeMetadata: { ...user.unsafeMetadata, theme: newTheme, }, }); }; return ( ); } ``` ❌ **Bad: Storing sensitive data in public metadata** ```typescript // ❌ Don't expose sensitive data in publicMetadata await clerkClient().users.updateUser(userId, { publicMetadata: { creditCardNumber: '4242424242424242', // ❌ Exposed in session token! ssn: '123-45-6789', // ❌ Never store PII here }, }); // ✅ Use privateMetadata instead await clerkClient().users.updateUser(userId, { privateMetadata: { stripePaymentMethodId: 'pm_123', encryptedSSN: encryptSSN('123-45-6789'), }, }); ``` ### User Profile Updates ✅ **Good: Updating user profile** ```typescript 'use client'; import { useUser } from '@clerk/nextjs'; import { useState } from 'react'; export default function EditProfile() { const { user } = useUser(); const [firstName, setFirstName] = useState(user?.firstName || ''); const [lastName, setLastName] = useState(user?.lastName || ''); const handleUpdate = async (e: React.FormEvent) => { e.preventDefault(); try { await user?.update({ firstName, lastName, }); alert('Profile updated!'); } catch (err) { console.error('Error updating profile:', err); } }; return (
setFirstName(e.target.value)} placeholder="First Name" /> setLastName(e.target.value)} placeholder="Last Name" />
); } ``` ✅ **Good: Uploading profile image** ```typescript 'use client'; import { useUser } from '@clerk/nextjs'; export default function ProfileImageUpload() { const { user } = useUser(); const handleImageUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; try { await user?.setProfileImage({ file }); alert('Profile image updated!'); } catch (err) { console.error('Error uploading image:', err); } }; return (
Profile
); } ``` ### User Deletion ✅ **Good: User account deletion** ```typescript 'use client'; import { useUser } from '@clerk/nextjs'; import { useRouter } from 'next/navigation'; export default function DeleteAccount() { const { user } = useUser(); const router = useRouter(); const handleDelete = async () => { if (!confirm('Are you sure you want to delete your account?')) { return; } try { await user?.delete(); router.push('/'); } catch (err) { console.error('Error deleting account:', err); } }; return ( ); } ``` ✅ **Good: Backend user deletion (admin)** ```typescript // app/api/admin/users/[userId]/route.ts import { clerkClient } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server'; import { auth } from '@clerk/nextjs/server'; export async function DELETE( req: Request, { params }: { params: { userId: string } } ) { const { userId: adminId } = await auth(); if (!adminId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } // Check if admin (from publicMetadata) const admin = await clerkClient().users.getUser(adminId); if (admin.publicMetadata.role !== 'admin') { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } await clerkClient().users.deleteUser(params.userId); return NextResponse.json({ success: true }); } ``` --- ## 4. Middleware & Route Protection ### Basic Middleware Setup ✅ **Good: Default protection with public routes** ```typescript // middleware.ts import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; const isPublicRoute = createRouteMatcher([ '/', '/sign-in(.*)', '/sign-up(.*)', '/about', '/contact', '/api/webhooks(.*)', ]); export default clerkMiddleware(async (auth, req) => { if (!isPublicRoute(req)) { await auth.protect(); } }); export const config = { matcher: [ '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', '/(api|trpc)(.*)', ], }; ``` ### Role-Based Access Control (RBAC) ✅ **Good: Protect routes by role** ```typescript // middleware.ts import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)']); const isAdminRoute = createRouteMatcher(['/admin(.*)']); const isModeratorRoute = createRouteMatcher(['/moderator(.*)']); export default clerkMiddleware(async (auth, req) => { const { userId, sessionClaims } = await auth(); if (isPublicRoute(req)) { return; } if (!userId) { return auth.redirectToSignIn(); } const role = sessionClaims?.metadata?.role as string; if (isAdminRoute(req) && role !== 'admin') { return Response.redirect(new URL('/403', req.url)); } if (isModeratorRoute(req) && !['admin', 'moderator'].includes(role)) { return Response.redirect(new URL('/403', req.url)); } }); ``` ✅ **Good: Custom session claims for RBAC** Configure in Clerk Dashboard → Sessions → Customize session token: ```json { "metadata": { "role": "{{user.public_metadata.role}}", "orgRole": "{{org.role}}" } } ``` ### Server-Side Route Protection ✅ **Good: Using auth().protect() in Server Components** ```typescript // app/dashboard/page.tsx import { auth } from '@clerk/nextjs/server'; import { redirect } from 'next/navigation'; export default async function DashboardPage() { const { userId } = await auth.protect(); // User is guaranteed to be authenticated here return
Dashboard for user: {userId}
; } ``` ✅ **Good: Conditional protection with role check** ```typescript // app/admin/page.tsx import { auth } from '@clerk/nextjs/server'; export default async function AdminPage() { const { userId, sessionClaims } = await auth.protect((has) => { return has({ role: 'admin' }); }); return
Admin Dashboard
; } ``` ✅ **Good: Organization-based protection** ```typescript // app/org/[orgId]/settings/page.tsx import { auth } from '@clerk/nextjs/server'; export default async function OrgSettingsPage({ params, }: { params: { orgId: string }; }) { await auth.protect((has) => { return ( has({ permission: 'org:settings:manage' }) || has({ role: 'org:admin' }) ); }); return
Organization Settings
; } ``` ### Redirect Patterns ✅ **Good: Custom redirects after authentication** ```typescript // app/sign-in/[[...sign-in]]/page.tsx import { SignIn } from '@clerk/nextjs'; export default function SignInPage() { return ( ); } ``` ✅ **Good: Conditional redirects based on metadata** ```typescript // middleware.ts import { clerkMiddleware } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server'; export default clerkMiddleware(async (auth, req) => { const { userId, sessionClaims } = await auth(); if (userId && req.nextUrl.pathname === '/sign-in') { const onboardingComplete = sessionClaims?.metadata?.onboardingComplete; if (!onboardingComplete) { return NextResponse.redirect(new URL('/onboarding', req.url)); } return NextResponse.redirect(new URL('/dashboard', req.url)); } }); ``` ### beforeAuth and afterAuth Patterns ✅ **Good: Rate limiting before auth** ```typescript // middleware.ts import { clerkMiddleware } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server'; export default clerkMiddleware(async (auth, req) => { // beforeAuth: Run logic before authentication const ip = req.headers.get('x-forwarded-for') || 'unknown'; const isRateLimited = await checkRateLimit(ip); if (isRateLimited) { return NextResponse.json( { error: 'Too many requests' }, { status: 429 } ); } // Authentication happens here const { userId } = await auth(); // afterAuth: Run logic after authentication if (userId) { await trackUserActivity(userId, req.url); } return NextResponse.next(); }); async function checkRateLimit(ip: string): Promise { // Implement rate limiting logic return false; } async function trackUserActivity(userId: string, url: string) { // Track user activity } ``` ❌ **Bad: Missing route protection** ```typescript // app/dashboard/page.tsx export default function DashboardPage() { // ❌ No auth check - anyone can access return
Secret dashboard data
; } ``` ❌ **Bad: Client-only protection** ```typescript 'use client'; import { useUser } from '@clerk/nextjs'; import { useRouter } from 'next/navigation'; export default function ProtectedPage() { const { isSignedIn } = useUser(); const router = useRouter(); // ❌ Client-side only - can be bypassed if (!isSignedIn) { router.push('/sign-in'); return null; } return
Protected content
; } // ✅ Always use middleware or server-side auth checks ``` --- ## 5. Session Management ### Session Tokens (JWT) Clerk generates short-lived JWT tokens for each authenticated session. **Default Claims:** ```typescript interface SessionClaims { azp: string; // Authorized party (origin) exp: number; // Expiration time iat: number; // Issued at iss: string; // Issuer (Clerk) nbf: number; // Not before sid: string; // Session ID sub: string; // User ID } ``` ### Server-Side Session Access ✅ **Good: Getting session info** ```typescript // app/api/me/route.ts import { auth } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server'; export async function GET() { const { userId, sessionId, sessionClaims } = await auth(); if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } return NextResponse.json({ userId, sessionId, claims: sessionClaims, }); } ``` ✅ **Good: Getting session token** ```typescript // app/api/external/route.ts import { auth } from '@clerk/nextjs/server'; export async function GET() { const { getToken } = await auth(); const token = await getToken(); // Use token to authenticate with external API const response = await fetch('https://api.example.com/data', { headers: { Authorization: `Bearer ${token}`, }, }); return Response.json(await response.json()); } ``` ### Client-Side Session Hooks ✅ **Good: useAuth hook** ```typescript 'use client'; import { useAuth } from '@clerk/nextjs'; export default function SessionInfo() { const { isLoaded, userId, sessionId, getToken, signOut, } = useAuth(); if (!isLoaded) { return
Loading...
; } const handleAPICall = async () => { const token = await getToken(); const response = await fetch('/api/protected', { headers: { Authorization: `Bearer ${token}`, }, }); }; return (

User ID: {userId}

Session ID: {sessionId}

); } ``` ✅ **Good: useSession hook** ```typescript 'use client'; import { useSession } from '@clerk/nextjs'; export default function SessionDetails() { const { session, isLoaded } = useSession(); if (!isLoaded) { return
Loading...
; } return (

Session ID: {session?.id}

Created: {session?.createdAt.toLocaleString()}

Last Active: {session?.lastActiveAt.toLocaleString()}

Expires: {session?.expireAt.toLocaleString()}

); } ``` ### Session Customization (Custom Claims) ✅ **Good: Adding custom claims to session token** Configure in Clerk Dashboard → Sessions → Customize session token: ```json { "role": "{{user.public_metadata.role}}", "subscription": "{{user.public_metadata.subscriptionTier}}", "orgId": "{{org.id}}", "orgRole": "{{org_membership.role}}", "permissions": "{{org_membership.permissions}}" } ``` ✅ **Good: Accessing custom claims** ```typescript // app/api/admin/route.ts import { auth } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server'; export async function GET() { const { sessionClaims } = await auth(); const role = sessionClaims?.role as string; const subscription = sessionClaims?.subscription as string; if (role !== 'admin') { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } return NextResponse.json({ message: 'Admin access granted', subscription, }); } ``` ### Session Lifecycle ✅ **Good: Manual session refresh** ```typescript 'use client'; import { useSession } from '@clerk/nextjs'; export default function RefreshSession() { const { session } = useSession(); const handleRefresh = async () => { // Force refresh session token await session?.getToken({ skipCache: true }); }; return ( ); } ``` ✅ **Good: Sign out with redirect** ```typescript 'use client'; import { useClerk } from '@clerk/nextjs'; export default function SignOutButton() { const { signOut } = useClerk(); return ( ); } ``` ❌ **Bad: Caching tokens too long** ```typescript // ❌ Don't cache tokens indefinitely const token = await getToken(); localStorage.setItem('token', token); // Tokens expire! // ✅ Always get fresh tokens const token = await getToken(); ``` --- ## 6. Organizations Organizations enable B2B multi-tenancy, allowing users to collaborate in teams. ### Creating Organizations ✅ **Good: Using prebuilt component** ```typescript // app/create-organization/page.tsx import { CreateOrganization } from '@clerk/nextjs'; export default function CreateOrgPage() { return (

Create Your Organization

); } ``` ✅ **Good: Programmatic organization creation** ```typescript 'use client'; import { useOrganizationList } from '@clerk/nextjs'; import { useState } from 'react'; export default function CreateOrgForm() { const { createOrganization } = useOrganizationList(); const [name, setName] = useState(''); const handleCreate = async (e: React.FormEvent) => { e.preventDefault(); try { const org = await createOrganization?.({ name }); console.log('Created org:', org); } catch (err) { console.error('Error creating org:', err); } }; return (
setName(e.target.value)} placeholder="Organization name" />
); } ``` ### Organization Roles and Permissions Default roles: - `org:admin` - Full organization access - `org:member` - Basic member access ✅ **Good: Custom organization roles** Configure in Clerk Dashboard → Organizations → Roles: ```json { "roles": [ { "key": "org:admin", "name": "Admin", "permissions": ["org:manage", "org:delete", "org:members:manage"] }, { "key": "org:billing_manager", "name": "Billing Manager", "permissions": ["org:billing:manage"] }, { "key": "org:member", "name": "Member", "permissions": ["org:read"] } ] } ``` ✅ **Good: Checking organization permissions** ```typescript // app/org/[orgId]/settings/page.tsx import { auth } from '@clerk/nextjs/server'; export default async function OrgSettingsPage() { const { has } = await auth(); const canManageSettings = has?.({ permission: 'org:manage' }); const canManageMembers = has?.({ permission: 'org:members:manage' }); if (!canManageSettings) { return
Access denied
; } return (

Organization Settings

{canManageMembers && (

Members

{/* Member management UI */}
)}
); } ``` ### Organization Invitations ✅ **Good: Inviting members** ```typescript 'use client'; import { useOrganization } from '@clerk/nextjs'; import { useState } from 'react'; export default function InviteMember() { const { organization } = useOrganization(); const [email, setEmail] = useState(''); const [role, setRole] = useState<'org:admin' | 'org:member'>('org:member'); const handleInvite = async (e: React.FormEvent) => { e.preventDefault(); try { await organization?.inviteMember({ emailAddress: email, role, }); alert(`Invited ${email} as ${role}`); setEmail(''); } catch (err) { console.error('Error inviting member:', err); } }; return (
setEmail(e.target.value)} placeholder="Email address" />
); } ``` ✅ **Good: Managing invitations** ```typescript 'use client'; import { useOrganization } from '@clerk/nextjs'; export default function PendingInvitations() { const { organization, invitationList } = useOrganization({ invitationList: {}, }); const revokeInvitation = async (invitationId: string) => { try { await organization?.revokeInvitation(invitationId); } catch (err) { console.error('Error revoking invitation:', err); } }; return (
    {invitationList?.data?.map((invitation) => (
  • {invitation.emailAddress} - {invitation.role}
  • ))}
); } ``` ### Organization Switching ✅ **Good: Organization switcher component** ```typescript // app/components/OrgSwitcher.tsx import { OrganizationSwitcher } from '@clerk/nextjs'; export default function OrgSwitcher() { return ( ); } ``` ✅ **Good: Programmatic organization switching** ```typescript 'use client'; import { useOrganizationList } from '@clerk/nextjs'; export default function OrgList() { const { setActive, userMemberships } = useOrganizationList({ userMemberships: { infinite: true, }, }); const switchOrg = async (orgId: string) => { await setActive?.({ organization: orgId }); }; return (
    {userMemberships.data?.map((membership) => (
  • ))}
); } ``` ### Organization Metadata ✅ **Good: Setting organization metadata** ```typescript // app/api/organizations/[orgId]/route.ts import { clerkClient } from '@clerk/nextjs/server'; import { NextRequest, NextResponse } from 'next/server'; export async function PATCH( req: NextRequest, { params }: { params: { orgId: string } } ) { const { subscriptionTier, features } = await req.json(); await clerkClient().organizations.updateOrganization(params.orgId, { publicMetadata: { subscriptionTier, features, }, }); return NextResponse.json({ success: true }); } ``` ❌ **Bad: Not checking organization membership** ```typescript // ❌ Anyone can access organization data export default async function OrgDashboard({ params, }: { params: { orgId: string }; }) { // No membership check! return
Org: {params.orgId}
; } // ✅ Always verify membership export default async function OrgDashboard({ params, }: { params: { orgId: string }; }) { const { orgId } = await auth(); if (orgId !== params.orgId) { return
Access denied
; } return
Org: {params.orgId}
; } ``` --- ## 7. Webhooks Clerk webhooks notify your application of events (user creation, updates, deletions, etc.). ### Webhook Events Common events: - `user.created` - `user.updated` - `user.deleted` - `session.created` - `session.ended` - `organization.created` - `organization.updated` - `organizationMembership.created` - `organizationMembership.deleted` ### Webhook Setup 1. **Configure endpoint** in Clerk Dashboard → Webhooks 2. **Add endpoint URL**: `https://yourdomain.com/api/webhooks/clerk` 3. **Subscribe to events**: Select events to receive 4. **Copy signing secret**: `whsec_...` ### Webhook Handler ✅ **Good: Svix signature verification** ```typescript // app/api/webhooks/clerk/route.ts import { Webhook } from 'svix'; import { headers } from 'next/headers'; import { WebhookEvent } from '@clerk/nextjs/server'; export async function POST(req: Request) { const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET; if (!WEBHOOK_SECRET) { throw new Error('CLERK_WEBHOOK_SECRET is not set'); } // Get headers const headerPayload = await headers(); const svix_id = headerPayload.get('svix-id'); const svix_timestamp = headerPayload.get('svix-timestamp'); const svix_signature = headerPayload.get('svix-signature'); if (!svix_id || !svix_timestamp || !svix_signature) { return new Response('Missing svix headers', { status: 400 }); } // Get body const payload = await req.json(); const body = JSON.stringify(payload); // Verify webhook const wh = new Webhook(WEBHOOK_SECRET); let evt: WebhookEvent; try { evt = wh.verify(body, { 'svix-id': svix_id, 'svix-timestamp': svix_timestamp, 'svix-signature': svix_signature, }) as WebhookEvent; } catch (err) { console.error('Webhook verification failed:', err); return new Response('Verification failed', { status: 400 }); } // Handle event const { id } = evt.data; const eventType = evt.type; console.log(`Webhook ${eventType} for ${id}`); return new Response('Webhook received', { status: 200 }); } ``` ### Syncing Users to Database ✅ **Good: User created webhook** ```typescript // app/api/webhooks/clerk/route.ts import { Webhook } from 'svix'; import { headers } from 'next/headers'; import { WebhookEvent } from '@clerk/nextjs/server'; import { db } from '@/lib/db'; export async function POST(req: Request) { const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET!; const headerPayload = await headers(); const svix_id = headerPayload.get('svix-id')!; const svix_timestamp = headerPayload.get('svix-timestamp')!; const svix_signature = headerPayload.get('svix-signature')!; const payload = await req.json(); const body = JSON.stringify(payload); const wh = new Webhook(WEBHOOK_SECRET); const evt = wh.verify(body, { 'svix-id': svix_id, 'svix-timestamp': svix_timestamp, 'svix-signature': svix_signature, }) as WebhookEvent; switch (evt.type) { case 'user.created': await db.user.create({ data: { clerkId: evt.data.id, email: evt.data.email_addresses[0]?.email_address, firstName: evt.data.first_name, lastName: evt.data.last_name, imageUrl: evt.data.image_url, }, }); break; case 'user.updated': await db.user.update({ where: { clerkId: evt.data.id }, data: { email: evt.data.email_addresses[0]?.email_address, firstName: evt.data.first_name, lastName: evt.data.last_name, imageUrl: evt.data.image_url, }, }); break; case 'user.deleted': await db.user.delete({ where: { clerkId: evt.data.id! }, }); break; } return new Response('Webhook processed', { status: 200 }); } ``` ✅ **Good: Organization webhooks** ```typescript switch (evt.type) { case 'organization.created': await db.organization.create({ data: { clerkId: evt.data.id, name: evt.data.name, slug: evt.data.slug, imageUrl: evt.data.image_url, }, }); break; case 'organizationMembership.created': await db.organizationMember.create({ data: { organizationId: evt.data.organization.id, userId: evt.data.public_user_data.user_id, role: evt.data.role, }, }); break; case 'organizationMembership.deleted': await db.organizationMember.delete({ where: { organizationId_userId: { organizationId: evt.data.organization.id, userId: evt.data.public_user_data.user_id, }, }, }); break; } ``` ❌ **Bad: No webhook verification** ```typescript // ❌ DANGEROUS: No signature verification export async function POST(req: Request) { const payload = await req.json(); // Anyone can send fake webhooks! await db.user.create({ data: payload.data }); return new Response('OK'); } // ✅ Always verify with Svix ``` ❌ **Bad: Blocking webhook processing** ```typescript // ❌ Long-running operations block webhook export async function POST(req: Request) { // Verify webhook... // This takes too long - webhook will timeout await sendWelcomeEmail(evt.data.id); await generateReport(evt.data.id); await updateAnalytics(evt.data.id); return new Response('OK'); } // ✅ Queue background jobs instead export async function POST(req: Request) { // Verify webhook... // Queue jobs for async processing await queue.add('welcome-email', { userId: evt.data.id }); return new Response('OK', { status: 200 }); } ``` --- ## 8. UI Components Clerk provides prebuilt components for rapid integration. ### SignIn Component ```typescript import { SignIn } from '@clerk/nextjs'; export default function SignInPage() { return ; } ``` ### SignUp Component ```typescript import { SignUp } from '@clerk/nextjs'; export default function SignUpPage() { return ; } ``` ### UserButton Component ```typescript import { UserButton } from '@clerk/nextjs'; export default function Header() { return (
); } ``` ### Component Customization ✅ **Good: Custom appearance** ```typescript import { SignIn } from '@clerk/nextjs'; export default function CustomSignIn() { return ( ); } ``` ✅ **Good: Using themes** ```typescript import { SignIn } from '@clerk/nextjs'; import { dark } from '@clerk/themes'; export default function ThemedSignIn() { return ( ); } ``` ✅ **Good: UserButton with custom menu items** ```typescript import { UserButton } from '@clerk/nextjs'; export default function Header() { return ( } href="/dashboard" /> } href="/settings" /> ); } ``` ❌ **Bad: Hardcoding theme in layout** ```typescript // ❌ Won't respond to theme changes export default function Layout() { return ( {children} ); } // ✅ Apply theme at component level 'use client'; import { useTheme } from 'next-themes'; import { SignIn } from '@clerk/nextjs'; import { dark } from '@clerk/themes'; export default function ThemedSignIn() { const { theme } = useTheme(); return ( ); } ``` --- ## Best Practices ### Environment Variables Security ✅ **Good: Use environment variables** ```bash # .env.local NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_... CLERK_SECRET_KEY=sk_live_... CLERK_WEBHOOK_SECRET=whsec_... ``` ❌ **Bad: Hardcoding keys** ```typescript // ❌ Never hardcode keys const clerkPublishableKey = 'pk_live_abc123...'; ``` ### TypeScript Types ✅ **Good: Type safety with custom metadata** ```typescript // types/clerk.ts declare global { interface CustomJwtSessionClaims { metadata: { role?: 'admin' | 'user' | 'moderator'; onboardingComplete?: boolean; }; } } export interface UserPublicMetadata { role: 'admin' | 'user' | 'moderator'; subscriptionTier: 'free' | 'pro' | 'enterprise'; } export interface UserPrivateMetadata { stripeCustomerId: string; } ``` ### Performance Optimization ✅ **Good: Selective data fetching** ```typescript 'use client'; import { useOrganization } from '@clerk/nextjs'; export default function OrgMembers() { // Only fetch what you need const { membershipList } = useOrganization({ membershipList: { limit: 10, offset: 0, }, }); return (
    {membershipList?.data?.map((membership) => (
  • {membership.publicUserData.firstName}
  • ))}
); } ``` ### Error Handling ✅ **Good: Comprehensive error handling** ```typescript 'use client'; import { useSignIn } from '@clerk/nextjs'; import { isClerkAPIResponseError } from '@clerk/nextjs/errors'; export default function SignInForm() { const { signIn } = useSignIn(); const handleSignIn = async (email: string, password: string) => { try { await signIn?.create({ identifier: email, password, }); } catch (err) { if (isClerkAPIResponseError(err)) { console.error('Clerk error:', err.errors); err.errors.forEach((error) => { if (error.code === 'form_password_incorrect') { alert('Incorrect password'); } }); } } }; } ``` --- ## Summary Clerk provides comprehensive authentication and user management for Next.js applications: 1. **Setup**: Install `@clerk/nextjs`, configure environment variables, wrap app with `ClerkProvider` 2. **Authentication**: Support for email/password, OAuth, magic links, SMS, and MFA 3. **User Management**: Rich user object with metadata (public/private/unsafe) 4. **Middleware**: Route protection with role-based access control 5. **Sessions**: JWT-based sessions with customizable claims 6. **Organizations**: B2B multi-tenancy with roles and permissions 7. **Webhooks**: Real-time event notifications with Svix verification 8. **UI Components**: Prebuilt, customizable components for rapid development Use environment variables for all sensitive data, implement proper server-side protection, and leverage Clerk's comprehensive feature set for production-ready authentication.