Clerk + Next.js Authentication

Complete authentication setup with Clerk for Next.js App Router.

Dependencies#

npm install @clerk/nextjs

Environment Variables#

1# .env.local 2NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... 3CLERK_SECRET_KEY=sk_test_... 4NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 5NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 6NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard 7NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard

Setup#

1. Clerk Provider#

1// app/layout.tsx 2import { ClerkProvider } from '@clerk/nextjs'; 3 4export default function RootLayout({ 5 children, 6}: { 7 children: React.ReactNode; 8}) { 9 return ( 10 <ClerkProvider> 11 <html lang="en"> 12 <body>{children}</body> 13 </html> 14 </ClerkProvider> 15 ); 16}

2. Middleware#

1// middleware.ts 2import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; 3 4const isPublicRoute = createRouteMatcher([ 5 '/', 6 '/sign-in(.*)', 7 '/sign-up(.*)', 8 '/api/webhooks(.*)', 9 '/api/public(.*)', 10]); 11 12export default clerkMiddleware(async (auth, request) => { 13 if (!isPublicRoute(request)) { 14 await auth.protect(); 15 } 16}); 17 18export const config = { 19 matcher: [ 20 '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', 21 '/(api|trpc)(.*)', 22 ], 23};

3. Sign In Page#

1// app/sign-in/[[...sign-in]]/page.tsx 2import { SignIn } from '@clerk/nextjs'; 3 4export default function SignInPage() { 5 return ( 6 <div className="flex min-h-screen items-center justify-center"> 7 <SignIn /> 8 </div> 9 ); 10}

4. Sign Up Page#

1// app/sign-up/[[...sign-up]]/page.tsx 2import { SignUp } from '@clerk/nextjs'; 3 4export default function SignUpPage() { 5 return ( 6 <div className="flex min-h-screen items-center justify-center"> 7 <SignUp /> 8 </div> 9 ); 10}

5. User Button#

1// components/Header.tsx 2import { SignedIn, SignedOut, UserButton, SignInButton } from '@clerk/nextjs'; 3 4export function Header() { 5 return ( 6 <header className="flex items-center justify-between p-4"> 7 <h1>My App</h1> 8 <div> 9 <SignedIn> 10 <UserButton afterSignOutUrl="/" /> 11 </SignedIn> 12 <SignedOut> 13 <SignInButton mode="modal"> 14 <button className="btn">Sign In</button> 15 </SignInButton> 16 </SignedOut> 17 </div> 18 </header> 19 ); 20}

Server-Side Authentication#

Get Current User#

1// app/dashboard/page.tsx 2import { currentUser } from '@clerk/nextjs/server'; 3import { redirect } from 'next/navigation'; 4 5export default async function DashboardPage() { 6 const user = await currentUser(); 7 8 if (!user) { 9 redirect('/sign-in'); 10 } 11 12 return ( 13 <div> 14 <h1>Welcome, {user.firstName}!</h1> 15 <p>Email: {user.emailAddresses[0].emailAddress}</p> 16 </div> 17 ); 18}

API Route Authentication#

1// app/api/user/route.ts 2import { auth, currentUser } from '@clerk/nextjs/server'; 3import { NextResponse } from 'next/server'; 4 5export async function GET() { 6 const { userId } = await auth(); 7 8 if (!userId) { 9 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 10 } 11 12 const user = await currentUser(); 13 14 return NextResponse.json({ 15 id: userId, 16 email: user?.emailAddresses[0].emailAddress, 17 name: `${user?.firstName} ${user?.lastName}`, 18 }); 19}

Sync Users to Database#

Webhook Handler#

1// app/api/webhooks/clerk/route.ts 2import { Webhook } from 'svix'; 3import { headers } from 'next/headers'; 4import { WebhookEvent } from '@clerk/nextjs/server'; 5import { prisma } from '@/lib/prisma'; 6 7export async function POST(req: Request) { 8 const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET; 9 10 if (!WEBHOOK_SECRET) { 11 throw new Error('CLERK_WEBHOOK_SECRET is not set'); 12 } 13 14 const headerPayload = await headers(); 15 const svix_id = headerPayload.get('svix-id'); 16 const svix_timestamp = headerPayload.get('svix-timestamp'); 17 const svix_signature = headerPayload.get('svix-signature'); 18 19 if (!svix_id || !svix_timestamp || !svix_signature) { 20 return new Response('Missing svix headers', { status: 400 }); 21 } 22 23 const payload = await req.json(); 24 const body = JSON.stringify(payload); 25 26 const wh = new Webhook(WEBHOOK_SECRET); 27 let evt: WebhookEvent; 28 29 try { 30 evt = wh.verify(body, { 31 'svix-id': svix_id, 32 'svix-timestamp': svix_timestamp, 33 'svix-signature': svix_signature, 34 }) as WebhookEvent; 35 } catch (err) { 36 console.error('Webhook verification failed:', err); 37 return new Response('Invalid signature', { status: 400 }); 38 } 39 40 switch (evt.type) { 41 case 'user.created': 42 await prisma.user.create({ 43 data: { 44 clerkId: evt.data.id, 45 email: evt.data.email_addresses[0].email_address, 46 name: `${evt.data.first_name} ${evt.data.last_name}`.trim(), 47 imageUrl: evt.data.image_url, 48 }, 49 }); 50 break; 51 52 case 'user.updated': 53 await prisma.user.update({ 54 where: { clerkId: evt.data.id }, 55 data: { 56 email: evt.data.email_addresses[0].email_address, 57 name: `${evt.data.first_name} ${evt.data.last_name}`.trim(), 58 imageUrl: evt.data.image_url, 59 }, 60 }); 61 break; 62 63 case 'user.deleted': 64 await prisma.user.delete({ 65 where: { clerkId: evt.data.id }, 66 }); 67 break; 68 } 69 70 return new Response('OK', { status: 200 }); 71}

Prisma Schema#

1// prisma/schema.prisma 2model User { 3 id String @id @default(cuid()) 4 clerkId String @unique 5 email String @unique 6 name String? 7 imageUrl String? 8 createdAt DateTime @default(now()) 9 updatedAt DateTime @updatedAt 10}

Custom Sign-In UI#

1// components/CustomSignIn.tsx 2'use client'; 3 4import { useSignIn } from '@clerk/nextjs'; 5import { useState } from 'react'; 6import { useRouter } from 'next/navigation'; 7 8export function CustomSignIn() { 9 const { isLoaded, signIn, setActive } = useSignIn(); 10 const [email, setEmail] = useState(''); 11 const [password, setPassword] = useState(''); 12 const [error, setError] = useState(''); 13 const router = useRouter(); 14 15 async function handleSubmit(e: React.FormEvent) { 16 e.preventDefault(); 17 if (!isLoaded) return; 18 19 try { 20 const result = await signIn.create({ 21 identifier: email, 22 password, 23 }); 24 25 if (result.status === 'complete') { 26 await setActive({ session: result.createdSessionId }); 27 router.push('/dashboard'); 28 } 29 } catch (err: any) { 30 setError(err.errors?.[0]?.message || 'Sign in failed'); 31 } 32 } 33 34 return ( 35 <form onSubmit={handleSubmit} className="space-y-4"> 36 {error && ( 37 <div className="p-3 text-red-600 bg-red-50 rounded-lg">{error}</div> 38 )} 39 40 <div> 41 <label htmlFor="email">Email</label> 42 <input 43 id="email" 44 type="email" 45 value={email} 46 onChange={(e) => setEmail(e.target.value)} 47 className="w-full p-2 border rounded" 48 required 49 /> 50 </div> 51 52 <div> 53 <label htmlFor="password">Password</label> 54 <input 55 id="password" 56 type="password" 57 value={password} 58 onChange={(e) => setPassword(e.target.value)} 59 className="w-full p-2 border rounded" 60 required 61 /> 62 </div> 63 64 <button 65 type="submit" 66 className="w-full p-2 bg-blue-600 text-white rounded" 67 > 68 Sign In 69 </button> 70 </form> 71 ); 72}

Customization#

Theme Customization#

1// app/layout.tsx 2import { ClerkProvider } from '@clerk/nextjs'; 3import { dark } from '@clerk/themes'; 4 5export default function RootLayout({ children }) { 6 return ( 7 <ClerkProvider 8 appearance={{ 9 baseTheme: dark, 10 variables: { 11 colorPrimary: '#6366f1', 12 colorBackground: '#1f2937', 13 }, 14 elements: { 15 formButtonPrimary: 'bg-indigo-600 hover:bg-indigo-700', 16 card: 'bg-gray-800 border-gray-700', 17 }, 18 }} 19 > 20 <html lang="en"> 21 <body>{children}</body> 22 </html> 23 </ClerkProvider> 24 ); 25}