Blog/Tutorials & Step-by-Step/Next.js App Router Authentication with Clerk
POST
April 10, 2025
LAST UPDATEDApril 10, 2025

Next.js App Router Authentication with Clerk

Complete guide to implementing authentication in Next.js App Router with Clerk — covering middleware, server components, webhooks, and protected routes.

Tags

Next.jsClerkAuthenticationApp Router
Next.js App Router Authentication with Clerk
7 min read

Next.js App Router Authentication with Clerk

In this tutorial, you will implement complete authentication in a Next.js App Router application using Clerk. You will set up protected routes with middleware, access user data in both server and client components, build custom sign-in and sign-up pages, and handle webhooks to sync user data with your database. By the end, you will have a production-ready auth system that covers every common authentication scenario.

TL;DR

Clerk handles authentication in Next.js App Router through three layers: middleware for route protection, server helpers for accessing user data in server components, and client components for sign-in flows. Set up takes about 15 minutes, and you get session management, MFA, and user management out of the box.

Prerequisites

  • Node.js 18 or later
  • A Next.js 14+ project with App Router
  • A Clerk account (sign up at clerk.com)
  • Basic familiarity with Next.js App Router concepts (server components, route handlers, middleware)

Step 1: Install and Configure Clerk

Start by installing the Clerk Next.js package:

bash
npm install @clerk/nextjs

Go to the Clerk dashboard, create an application, and copy your API keys. Add them to your .env.local file:

bash
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your-key-here
CLERK_SECRET_KEY=sk_test_your-key-here
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up

The publishable key is safe to expose to the client. The secret key must never leave the server. The sign-in and sign-up URL environment variables tell Clerk where to redirect unauthenticated users.

Step 2: Add the ClerkProvider

Wrap your application in the ClerkProvider component. This goes in your root layout:

typescript
// src/app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";
import type { Metadata } from "next";
import "./globals.css";
 
export const metadata: Metadata = {
  title: "My App",
  description: "Authenticated Next.js application",
};
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}

The ClerkProvider initializes the Clerk client, manages the authentication state, and makes auth context available to all child components. It must be at the root of your component tree.

Step 3: Set Up Middleware for Route Protection

Clerk's middleware intercepts requests and checks authentication status before the page renders. Create src/middleware.ts:

typescript
// src/middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
 
const isPublicRoute = createRouteMatcher([
  "/",
  "/sign-in(.*)",
  "/sign-up(.*)",
  "/api/webhooks(.*)",
]);
 
export default clerkMiddleware(async (auth, request) => {
  if (!isPublicRoute(request)) {
    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)(.*)",
  ],
};

The createRouteMatcher function defines patterns for public routes. Everything not matching these patterns requires authentication. When an unauthenticated user hits a protected route, auth.protect() automatically redirects them to the sign-in page.

The config.matcher tells Next.js which paths should run the middleware. It excludes static files and assets, which do not need authentication checks. The webhook route is public because Clerk needs to send events to it without authentication.

Step 4: Build Custom Sign-In and Sign-Up Pages

Clerk provides pre-built components that handle the entire authentication flow including OAuth, email/password, magic links, and MFA.

Create the sign-in page:

typescript
// src/app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from "@clerk/nextjs";
 
export default function SignInPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignIn
        appearance={{
          elements: {
            rootBox: "mx-auto",
            card: "shadow-lg",
          },
        }}
      />
    </div>
  );
}

Create the sign-up page:

typescript
// src/app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from "@clerk/nextjs";
 
export default function SignUpPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignUp
        appearance={{
          elements: {
            rootBox: "mx-auto",
            card: "shadow-lg",
          },
        }}
      />
    </div>
  );
}

The [[...sign-in]] catch-all route pattern is required because Clerk's sign-in flow can have multiple steps (password entry, MFA verification, OAuth callbacks). Each step renders at a different sub-path, and the catch-all route ensures they all render the same component.

The appearance prop lets you customize the look of Clerk's components to match your design system. You can override individual CSS classes, use Tailwind utilities, or apply a complete theme.

Step 5: Access User Data in Server Components

One of Clerk's strengths is first-class support for server components. You can access the current user without any client-side JavaScript:

typescript
// src/app/dashboard/page.tsx
import { auth, currentUser } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";
 
export default async function DashboardPage() {
  const { userId } = await auth();
 
  if (!userId) {
    redirect("/sign-in");
  }
 
  const user = await currentUser();
 
  return (
    <div className="p-8">
      <h1 className="text-2xl font-bold">Dashboard</h1>
      <div className="mt-4 space-y-2">
        <p>
          Welcome, {user?.firstName} {user?.lastName}
        </p>
        <p className="text-gray-600">{user?.emailAddresses[0]?.emailAddress}</p>
        <p className="text-sm text-gray-400">User ID: {userId}</p>
      </div>
    </div>
  );
}

The auth() function is lightweight and returns just the session information. Use it when you only need the userId for database queries. The currentUser() function makes an API call to Clerk to fetch the full user profile. Use it when you need to display user details. Since this runs entirely on the server, the user's data never touches the client bundle.

Step 6: Access User Data in Client Components

For interactive components that need auth state, Clerk provides client-side hooks:

typescript
// src/components/UserMenu.tsx
"use client";
 
import { useUser, useClerk } from "@clerk/nextjs";
import { useRouter } from "next/navigation";
 
export default function UserMenu() {
  const { user, isLoaded } = useUser();
  const { signOut } = useClerk();
  const router = useRouter();
 
  if (!isLoaded) {
    return <div className="h-8 w-8 animate-pulse rounded-full bg-gray-200" />;
  }
 
  if (!user) return null;
 
  return (
    <div className="flex items-center gap-4">
      <img
        src={user.imageUrl}
        alt={user.fullName ?? "User avatar"}
        className="h-8 w-8 rounded-full"
      />
      <span className="text-sm font-medium">{user.fullName}</span>
      <button
        onClick={() => signOut(() => router.push("/"))}
        className="text-sm text-gray-500 hover:text-gray-700"
      >
        Sign out
      </button>
    </div>
  );
}

The useUser hook provides the current user object and a loading state. Always check isLoaded before rendering user-dependent UI to avoid hydration mismatches. The useClerk hook gives access to the Clerk instance for actions like signing out.

Step 7: Protect API Routes

API routes need authentication too. Use the auth() function in route handlers:

typescript
// src/app/api/user/profile/route.ts
import { auth, currentUser } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
 
export async function GET() {
  const { userId } = await auth();
 
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  const user = await currentUser();
 
  return NextResponse.json({
    id: userId,
    email: user?.emailAddresses[0]?.emailAddress,
    name: user?.fullName,
  });
}
 
export async function PATCH(req: Request) {
  const { userId } = await auth();
 
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  const body = await req.json();
 
  // Update user in your database using userId
  // await db.update(users).set(body).where(eq(users.clerkId, userId));
 
  return NextResponse.json({ success: true });
}

Every API route that handles user-specific data should verify the userId at the top. This is a defense-in-depth measure, since the middleware already protects these routes, but it ensures security even if the middleware configuration changes.

Step 8: Handle Webhooks for User Sync

Clerk webhooks let you sync user data with your database. When a user signs up, updates their profile, or deletes their account, Clerk sends an event to your webhook endpoint.

Install the Svix library for webhook verification:

bash
npm install svix

Create the webhook handler:

typescript
// src/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("Missing CLERK_WEBHOOK_SECRET environment variable");
  }
 
  const headerPayload = await headers();
  const svixId = headerPayload.get("svix-id");
  const svixTimestamp = headerPayload.get("svix-timestamp");
  const svixSignature = headerPayload.get("svix-signature");
 
  if (!svixId || !svixTimestamp || !svixSignature) {
    return new Response("Missing svix headers", { status: 400 });
  }
 
  const payload = await req.json();
  const body = JSON.stringify(payload);
 
  const wh = new Webhook(WEBHOOK_SECRET);
  let event: WebhookEvent;
 
  try {
    event = wh.verify(body, {
      "svix-id": svixId,
      "svix-timestamp": svixTimestamp,
      "svix-signature": svixSignature,
    }) as WebhookEvent;
  } catch {
    return new Response("Invalid webhook signature", { status: 400 });
  }
 
  switch (event.type) {
    case "user.created": {
      const { id, email_addresses, first_name, last_name, image_url } =
        event.data;
      // await db.insert(users).values({
      //   clerkId: id,
      //   email: email_addresses[0]?.email_address,
      //   firstName: first_name,
      //   lastName: last_name,
      //   imageUrl: image_url,
      // });
      console.log("User created:", id);
      break;
    }
 
    case "user.updated": {
      const { id, email_addresses, first_name, last_name, image_url } =
        event.data;
      // await db.update(users).set({
      //   email: email_addresses[0]?.email_address,
      //   firstName: first_name,
      //   lastName: last_name,
      //   imageUrl: image_url,
      // }).where(eq(users.clerkId, id));
      console.log("User updated:", id);
      break;
    }
 
    case "user.deleted": {
      const { id } = event.data;
      // await db.delete(users).where(eq(users.clerkId, id));
      console.log("User deleted:", id);
      break;
    }
  }
 
  return new Response("Webhook processed", { status: 200 });
}

The webhook verification step is critical. Without it, anyone could send fake events to your endpoint. The Svix library validates the signature against your webhook secret to ensure the event genuinely came from Clerk.

In the Clerk dashboard, go to Webhooks, add an endpoint pointing to your deployed URL (e.g., https://yourapp.com/api/webhooks/clerk), and select the events you want to receive. Copy the signing secret into your .env.local as CLERK_WEBHOOK_SECRET.

The Complete Auth Flow

Here is how the full authentication flow works in your application:

  1. An unauthenticated user visits /dashboard
  2. The middleware detects no session and redirects to /sign-in
  3. The user signs in via Clerk's UI (OAuth, email/password, etc.)
  4. Clerk creates a session and redirects back to /dashboard
  5. The middleware allows the request through since the session is valid
  6. The server component calls auth() to get the userId and fetches data
  7. Clerk sends a user.created webhook to sync the user to your database

Every subsequent request includes the session cookie, and the middleware validates it before the page renders.

Next Steps

  • Role-based access control: Use Clerk's organizations feature or custom session claims to implement roles and permissions
  • Multi-factor authentication: Enable MFA in the Clerk dashboard for additional account security
  • Social connections: Add more OAuth providers like GitHub, Discord, or Apple from the Clerk dashboard
  • Custom session claims: Add custom data to session tokens for use in middleware and server components
  • Organization support: Use Clerk's organizations feature for multi-tenant applications

FAQ

What is Clerk and why use it with Next.js?

Clerk is a complete authentication and user management platform that provides pre-built UI components, session management, and multi-factor authentication. It integrates deeply with Next.js App Router, offering middleware for route protection, server component helpers for accessing user data, and client components for sign-in flows — eliminating the need to build and maintain your own auth system.

How does Clerk middleware protect routes in Next.js?

Clerk's middleware runs before every request matched by the config matcher. It uses clerkMiddleware and createRouteMatcher to define which routes are public and which require authentication. Unauthenticated users hitting protected routes are automatically redirected to the sign-in page. The middleware runs on the edge, so authentication checks add minimal latency.

Can I access user data in server components with Clerk?

Yes, Clerk provides the auth() and currentUser() functions for server components. The auth() function returns the userId and session claims synchronously. The currentUser() function returns the full user object including email, name, and metadata. Both functions work without any client-side JavaScript.

How do Clerk webhooks work for syncing user data?

Clerk sends webhook events to your API endpoint when users are created, updated, or deleted. You verify the webhook signature using the svix library, parse the event payload, and sync the user data to your database. This keeps your application database in sync with Clerk's user records without polling.

Is Clerk free to use?

Clerk offers a free tier that includes up to 10,000 monthly active users with core authentication features. Paid plans add features like custom domains, advanced MFA options, and higher usage limits. For most projects getting started, the free tier is more than sufficient.

Collaboration

Need help with a project?

Let's Build It

I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI — let's bring your vision to life.

SH

Article Author

Sadam Hussain

Senior Full Stack Developer

Senior Full Stack Developer with over 7 years of experience building React, Next.js, Node.js, TypeScript, and AI-powered web platforms.

Related Articles

How to Add Observability to a Node.js App with OpenTelemetry
Mar 21, 20265 min read
Node.js
OpenTelemetry
Observability

How to Add Observability to a Node.js App with OpenTelemetry

Learn how to instrument a Node.js app with OpenTelemetry for traces, metrics, and logs, and build a practical observability setup for production debugging.

How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js
Mar 21, 20266 min read
Next.js
Node.js
BFF

How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js

A practical guide to building a Backend-for-Frontend with Next.js and Node.js for API aggregation, auth handling, caching, and frontend-specific data shaping.

How I Structure CI/CD for Next.js, Docker, and GitHub Actions
Mar 21, 20265 min read
CI/CD
Next.js
Docker

How I Structure CI/CD for Next.js, Docker, and GitHub Actions

A practical CI/CD blueprint for Next.js apps using Docker and GitHub Actions, including testing, image builds, deployment stages, cache strategy, and release safety.