Clerk + Next.js Authentication
Complete authentication setup with Clerk for Next.js App Router.
Dependencies#
npm install @clerk/nextjsEnvironment 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=/dashboardSetup#
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}