Back to Guide

Thursday, June 12, 2025

Clerk Auth v5 Integration Cheatsheet for Next.js App

cover

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

1.4. Wrap Your Application with ClerkProvider

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>
  );
}

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.

4.1. <SignInButton /> & <SignUpButton />

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>
  );
}

4.2. <UserButton />

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.

4.3. <SignedIn /> & <SignedOut />

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.

4.4. <Protect />

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

  1. Go to your Clerk Dashboard -> Webhooks.

  2. Click "Add Endpoint".

  3. Endpoint URL: During local development, use a tunneling service like ngrok. Run ngrok http 3000 (if your Next.js app runs on port 3000) and use the https:// forwarding URL provided by ngrok (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).

  4. Subscribe to events: Select the events you want to listen for, e.g., user.created, user.updated, user.deleted, organization.created, etc.

  5. Click "Create".

  6. 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.

Subscribe to be the first to receive the new Guide when it comes out

•Get All Guides at Once