Thursday, June 12, 2025
Clerk Auth v5 Integration Cheatsheet for Next.js App
Posted by
Clerk Auth v5 Integration Cheatsheet for Next.js App
This cheatsheet provides a comprehensive guide to integrating Clerk authentication v5 into your Next.js application, covering common use cases from setup to advanced data synchronization with webhooks, with a special focus on protecting server pages without explicit middleware.
1. Initial Setup and Configuration
1.1. Create a Next.js Project (if you haven't already)
npx create-next-app@latest my-clerk-app --typescript --eslint --tailwind
cd my-clerk-app
1.2. Install Clerk SDK
npm install @clerk/nextjs
# or
yarn add @clerk/nextjs
# or
pnpm add @clerk/nextjs
1.3. Configure Environment Variables
Obtain your NEXT_PUBLIC_CLERK_FRONTEND_API
and CLERK_SECRET_KEY
from your Clerk Dashboard. Create a .env.local
file in the root of your project:
NEXT_PUBLIC_CLERK_FRONTEND_API=pk_test_YOUR_CLERK_FRONTEND_API_KEY
CLERK_SECRET_KEY=sk_test_YOUR_CLERK_SECRET_KEY
ClerkProvider
1.4. Wrap Your Application with Wrap your RootLayout
(app/layout.tsx
) with ClerkProvider
to provide Clerk's authentication context to your app.
// app/layout.tsx
import './globals.css'; // Your global styles
import { ClerkProvider } from '@clerk/nextjs';
import { Inter } from 'next/font/google'; // Or any font you prefer
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'Clerk Next.js App',
description: 'Clerk authentication with Next.js',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider>
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
</ClerkProvider>
);
}
middleware.ts
Setup
1.5. (Optional but Recommended for Global Auth) While the request specifies protecting server pages without middleware for specific pages, clerkMiddleware()
is generally used to enable Clerk's authentication features across your Next.js application, making auth()
and currentUser()
available in Server Components, Route Handlers, and Server Actions.
Create middleware.ts
in your project root or src/middleware.ts
:
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
// Define public routes that do not require authentication
const isPublicRoute = createRouteMatcher([
'/', // Your homepage
'/sign-in(.*)',
'/sign-up(.*)',
'/api/webhooks/clerk', // Webhook endpoint must be public
]);
export default clerkMiddleware((auth, req) => {
if (!isPublicRoute(req)) {
// If the route is not public, protect it
auth().protect();
}
});
export const config = {
// Matcher for all routes except static files and _next internals
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
Note: This middleware.ts
sets up the global Clerk middleware. The section on "Protecting Server-side Pages without Middleware" later will refer to how to protect individual server components/pages within this enabled context, without writing additional per-page middleware logic.
2. Custom Sign-in and Sign-up Pages
Clerk allows you to host the pre-built UI components on your own custom routes.
2.1. Create Custom Sign-in Page
Create a file app/sign-in/[[...sign-in]]/page.tsx
:
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';
export default function Page() {
return (
<div className="flex justify-center items-center min-h-screen">
<SignIn />
</div>
);
}
2.2. Create Custom Sign-up Page
Create a file app/sign-up/[[...sign-up]]/page.tsx
:
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs';
export default function Page() {
return (
<div className="flex justify-center items-center min-h-screen">
<SignUp />
</div>
);
}
2.3. Update Environment Variables for Custom Paths
To inform Clerk about your custom sign-in and sign-up URLs, update your .env.local
file:
# ... (existing Clerk keys) ...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard # Redirect after sign-in
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding # Redirect after sign-up
3. Profile Pages
Clerk provides a <UserProfile />
component to easily manage user profiles.
3.1. Create a Profile Page
Create a file app/user-profile/[[...user-profile]]/page.tsx
:
// app/user-profile/[[...user-profile]]/page.tsx
import { UserProfile } from '@clerk/nextjs';
export default function UserProfilePage() {
return (
<div className="flex justify-center items-center min-h-screen">
<UserProfile routing="path" path="/user-profile" />
</div>
);
}
Note: routing="path"
and path="/user-profile"
are important for the App Router to handle nested routes within the UserProfile component.
4. Common Clerk Components
Clerk offers several pre-built UI components to quickly add authentication features.
<SignInButton />
& <SignUpButton />
4.1. Used to trigger the sign-in/sign-up flow, typically redirecting to your custom pages.
// Example usage in a header component
import { SignInButton, SignUpButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs';
import Link from 'next/link';
export function Header() {
return (
<header className="flex justify-between items-center p-4 bg-gray-800 text-white">
<Link href="/" className="text-xl font-bold">My App</Link>
<nav>
<SignedOut>
<SignInButton mode="modal">
<button className="px-4 py-2 mr-2 bg-blue-600 rounded hover:bg-blue-700">Sign In</button>
</SignInButton>
<SignUpButton mode="modal">
<button className="px-4 py-2 bg-green-600 rounded hover:bg-green-700">Sign Up</button>
</SignUpButton>
</SignedOut>
<SignedIn>
<UserButton afterSignOutUrl="/" />
</SignedIn>
</nav>
</header>
);
}
<UserButton />
4.2. A button that provides a dropdown menu for users to manage their account, sign out, etc.
// See example above in Header component.
// It typically includes links to profile management.
<SignedIn />
& <SignedOut />
4.3. Components that conditionally render their children based on the user's authentication status.
// See example above in Header component.
// <SignedIn> content only visible if user is signed in.
// <SignedOut> content only visible if user is signed out.
<Protect />
4.4. Conditionally renders content based on user authentication and authorization (roles/permissions).
// app/dashboard/admin/page.tsx
import { Protect } from '@clerk/nextjs';
export default function AdminPage() {
return (
<Protect
role="org:admin" // Checks if user has 'admin' role in an organization
fallback={<p>You do not have permission to view this page.</p>}
>
<h1 className="text-2xl font-bold">Admin Dashboard</h1>
<p>Welcome, admin! Here's your restricted content.</p>
</Protect>
);
}
5. Protecting Client-side Pages
For client-side components (marked with 'use client'
), you can use useAuth
or useUser
hooks for programmatic access to authentication state and perform redirects.
// app/dashboard/client-protected-page/page.tsx
'use client';
import { useAuth } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
export default function ClientProtectedPage() {
const { isLoaded, isSignedIn } = useAuth();
const router = useRouter();
useEffect(() => {
if (isLoaded && !isSignedIn) {
// Redirect to sign-in page if not signed in
router.push('/sign-in');
}
}, [isLoaded, isSignedIn, router]);
if (!isLoaded || !isSignedIn) {
// Show a loading state or nothing while redirecting
return <p>Loading...</p>;
}
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Client-Side Protected Page</h1>
<p>This content is only visible to signed-in users.</p>
</div>
);
}
6. Protecting Server-side Pages (without middleware for the specific page)
Even with clerkMiddleware()
configured globally, you can protect individual Server Components, Route Handlers, and Server Actions directly within their respective files without writing separate middleware.ts
logic for each. You use the auth()
or currentUser()
helpers from @clerk/nextjs/server
.
6.1. Protecting a Server Component Page
// app/protected-server-page/page.tsx
import { auth, currentUser } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
export default async function ProtectedServerPage() {
const { userId } = auth(); // Get the current user's ID
const user = await currentUser(); // Get the full user object
if (!userId) {
// If no user is signed in, redirect to the sign-in page
redirect('/sign-in');
}
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Server-Side Protected Page</h1>
<p>Hello, {user?.firstName || 'User'}! You are signed in.</p>
<p>Your Clerk User ID: {userId}</p>
<p>This page was rendered on the server, only for authenticated users.</p>
</div>
);
}
6.2. Protecting a Server Action
// app/actions.ts
'use server';
import { auth, currentUser } from '@clerk/nextjs/server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const { userId } = auth(); // Access auth state in a Server Action
if (!userId) {
// Handle unauthenticated user, e.g., throw an error or redirect
throw new Error('You must be signed in to create a post.');
}
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// In a real app, you would save this to a database
console.log(`User ${userId} created a post: ${title} - ${content}`);
// Revalidate the path to show updated content if applicable
revalidatePath('/posts');
}
export async function getUserDataForServerAction() {
const user = await currentUser(); // Access current user in a Server Action
if (!user) {
return null;
}
return {
id: user.id,
email: user.emailAddresses[0]?.emailAddress,
fullName: user.fullName,
};
}
7. Accessing Clerk Data
7.1. In Client Components
Use the useAuth()
and useUser()
hooks.
// components/UserInfoClient.tsx
'use client';
import { useAuth, useUser } from '@clerk/nextjs';
export default function UserInfoClient() {
const { isLoaded, isSignedIn, userId, sessionId, getToken } = useAuth();
const { user } = useUser();
if (!isLoaded) {
return <p>Loading user info...</p>;
}
if (!isSignedIn) {
return <p>Not signed in.</p>;
}
return (
<div className="p-4 border rounded shadow">
<h2 className="text-xl font-semibold">Client Component User Info</h2>
<p>User ID: {userId}</p>
<p>Session ID: {sessionId}</p>
<p>Full Name: {user?.fullName}</p>
<p>Email: {user?.emailAddresses[0]?.emailAddress}</p>
<button
onClick={async () => {
const token = await getToken();
console.log('Auth Token:', token);
alert('Check console for auth token!');
}}
className="mt-2 px-3 py-1 bg-purple-600 text-white rounded hover:bg-purple-700"
>
Get Auth Token
</button>
</div>
);
}
7.2. In Server Components, Route Handlers, and Server Actions
Use the auth()
and currentUser()
helpers from @clerk/nextjs/server
. These are async
functions.
// app/server-info/page.tsx (Server Component)
import { auth, currentUser } from '@clerk/nextjs/server';
export default async function ServerInfoPage() {
const { userId, sessionId, orgId } = auth(); // Get basic auth data
const user = await currentUser(); // Get full user object
if (!userId) {
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Server-Side User Info</h1>
<p>Not signed in. No user data available on the server.</p>
</div>
);
}
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Server-Side User Info</h1>
<p>User ID: {userId}</p>
<p>Session ID: {sessionId}</p>
{orgId && <p>Organization ID: {orgId}</p>}
<p>Full Name: {user?.fullName}</p>
<p>Email: {user?.emailAddresses[0]?.emailAddress}</p>
<p>This information was fetched directly on the server.</p>
</div>
);
}
// app/api/user-data/route.ts (Route Handler)
import { auth, currentUser } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
export async function GET() {
const { userId } = auth();
const user = await currentUser();
if (!userId) {
return new NextResponse('Unauthorized', { status: 401 });
}
return NextResponse.json({
userId,
user: user ? {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.emailAddresses[0]?.emailAddress,
imageUrl: user.imageUrl,
} : null,
});
}
8. Using Clerk Data in Your App with Webhooks
Webhooks are the recommended way to keep your application's database in sync with Clerk's user data.
8.1. Configure a Webhook in Clerk Dashboard
-
Go to your Clerk Dashboard -> Webhooks.
-
Click "Add Endpoint".
-
Endpoint URL: During local development, use a tunneling service like
ngrok
. Runngrok http 3000
(if your Next.js app runs on port 3000) and use thehttps://
forwarding URL provided byngrok
(e.g.,https://your-ngrok-url.ngrok-free.app/api/webhooks/clerk
). For production, this will be your deployed app's URL (e.g.,https://your-domain.com/api/webhooks/clerk
). -
Subscribe to events: Select the events you want to listen for, e.g.,
user.created
,user.updated
,user.deleted
,organization.created
, etc. -
Click "Create".
-
Copy the Signing Secret: After creation, you'll be redirected to the endpoint's settings page. Copy the
Signing Secret
.
8.2. Add Webhook Secret to Environment Variables
Add the copied Signing Secret to your .env.local
file:
# ... (existing Clerk keys) ...
CLERK_WEBHOOK_SECRET=whsec_YOUR_WEBHOOK_SIGNING_SECRET
8.3. Create a Webhook Route Handler in Next.js
Create a file app/api/webhooks/clerk/route.ts
. This route will receive the webhook payloads from Clerk.
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix'; // For verifying webhooks
import { headers } from 'next/headers';
import { WebhookEvent } from '@clerk/nextjs/server'; // Clerk's webhook event types
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
// 1. Get the headers
const headerPayload = headers();
const svix_id = headerPayload.get('svix-id');
const svix_timestamp = headerPayload.get('svix-timestamp');
const svix_signature = headerPayload.get('svix-signature');
// If there are no headers, error out
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response('Error: No Svix headers', { status: 400 });
}
// 2. Get the body
const payload = await req.json();
const body = JSON.stringify(payload);
// 3. Get the webhook secret
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
if (!WEBHOOK_SECRET) {
console.error('CLERK_WEBHOOK_SECRET is not set');
return new Response('Error: Clerk webhook secret not set', { status: 500 });
}
// 4. Create a new Svix instance with your secret
const wh = new Webhook(WEBHOOK_SECRET);
let evt: WebhookEvent;
// 5. Verify the payload with the headers
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('Error: Invalid webhook signature', { status: 400 });
}
// 6. Process the webhook event
const { id } = evt.data;
const eventType = evt.type;
console.log(`Webhook with ID of ${id} and type of ${eventType}`);
console.log('Webhook Body:', body);
// Example: Handle user creation event
if (eventType === 'user.created') {
const { id, first_name, last_name, email_addresses, image_url } = evt.data;
// In a real application, you would save this user data to your database
console.log(`New user created: ID ${id}, Name: ${first_name} ${last_name}, Email: ${email_addresses[0]?.email_address}`);
// Example: await db.users.create({ data: { clerkId: id, email: email_addresses[0].email_address, name: `${first_name} ${last_name}` } });
} else if (eventType === 'user.updated') {
const { id, first_name, last_name, email_addresses, image_url } = evt.data;
console.log(`User updated: ID ${id}, Name: ${first_name} ${last_name}`);
// Example: await db.users.update({ where: { clerkId: id }, data: { email: email_addresses[0].email_address, name: `${first_name} ${last_name}` } });
} else if (eventType === 'user.deleted') {
const { id } = evt.data;
console.log(`User deleted: ID ${id}`);
// Example: await db.users.delete({ where: { clerkId: id } });
// Or mark as deleted: await db.users.update({ where: { clerkId: id }, data: { deleted: true } });
}
// Acknowledge the webhook
return new NextResponse('Webhook received', { status: 200 });
}
8.4. Install Svix
To verify webhooks, you need the svix
library:
npm install svix
# or
yarn add svix
# or
pnpm add svix
Subscribe to My Latest Guides
All the latest Guides and tutorials, straight from the team.