Clerk Integration Guide

Technology: clerk · Category: auth · Last reviewed: 2026-06-18

Source: https://tech-stack.codeamanilabs.org/guide/clerk

Insight:

Clerk is the managed auth layer — drop-in Next.js components handle sign-in, sessions, and orgs so you don't roll your own. Current major is Clerk Core 3 (@clerk/nextjs v7): <ClerkProvider> now mounts inside <body> and the old <SignedIn>/<SignedOut>/<Protect> components are gone (use <Show>). Verify Clerk webhooks with Svix before trusting them, and consider an SMS-OTP fallback (Africa's Talking) for users without reliable email. Self-hosted alternative when per-MRU pricing or a JOIN-able user table matters: better-auth/CLAUDE_CODE_INTEGRATION.md.

 ██████╗██╗     ███████╗██████╗ ██╗  ██╗
██╔════╝██║     ██╔════╝██╔══██╗██║ ██╔╝
██║     ██║     █████╗  ██████╔╝█████╔╝
██║     ██║     ██╔══╝  ██╔══██╗██╔═██╗
╚██████╗███████╗███████╗██║  ██║██║  ██╗
 ╚═════╝╚══════╝╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝

Clerk Integration Guide

Focus: Integrating Clerk authentication into projects from Claude Code, using the official Clerk MCP server for SDK context, and automating user management workflows.

Overview

Clerk is a complete authentication and user management platform with pre-built UI components, JWT session management, webhooks, OAuth, and MFA. Its official MCP server provides Claude Code with up-to-date SDK snippets, implementation patterns, and integration guidance — ensuring Claude generates correct Clerk code rather than outdated patterns. Clerk also supports acting as an OAuth provider for MCP servers, enabling users to securely authorize AI agents to access your app's data.

Official Documentation

Resource URL
Clerk Docs https://clerk.com/docs
Clerk MCP Server https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server
Using Clerk with AI https://clerk.com/docs/guides/ai/overview
Next.js Quickstart https://clerk.com/docs/quickstarts/nextjs
Webhooks https://clerk.com/docs/integrations/webhooks
REST API https://clerk.com/docs/reference/backend-api

MCP Server Setup

Official Clerk MCP Server

Clerk provides a remote MCP server that gives Claude Code access to current SDK documentation, code snippets, and implementation patterns.

# Add Clerk MCP server to Claude Code
claude mcp add clerk -- npx -y mcp-remote https://mcp.clerk.com/mcp

.mcp.json Configuration

{
  "mcpServers": {
    "clerk": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://mcp.clerk.com/mcp"]
    }
  }
}

What the Clerk MCP Server Provides

Capability Description
SDK snippets Up-to-date @clerk/nextjs, @clerk/express, @clerk/backend examples
Component usage <SignIn>, <UserButton>, <ClerkProvider> patterns
Auth helpers auth(), currentUser(), getAuth() usage
Webhook setup Svix-verified webhook handler patterns
OAuth flows Social provider configuration examples
RBAC patterns Role and permission implementation guides

SDK Integration

Next.js (App Router)

Here is the core request flow that ties these pieces together — once you see it, the wiring below clicks into place.

flowchart TD
  A["User hits a route"] --> B["clerkMiddleware runs"]
  B --> Q1{"Public route?"}
  Q1 -->|"yes"| C["Allow through"]
  Q1 -->|"no"| D["auth.protect"]
  D --> Q2{"Valid session?"}
  Q2 -->|"yes"| E["Render protected page"]
  Q2 -->|"no"| F["Redirect to sign-in"]
  E --> G["auth and currentUser read userId"]

Requirements (Core 3): Node.js ≥ 20.9.0 and Next.js ≥ 15.2.3 (Next.js 13/14 are no longer supported).

# Recommended: the CLI scaffolds middleware, layout, and .env keys for you
npx -y clerk@latest init
# …or install manually
npm install @clerk/nextjs
# Verify the wiring afterwards
npx -y clerk@latest doctor

app/layout.tsx:

import { ClerkProvider } from "@clerk/nextjs";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {/* Core 3: ClerkProvider mounts INSIDE <body>, not wrapping <html>. */}
        <ClerkProvider>{children}</ClerkProvider>
      </body>
    </html>
  );
}

middleware.ts:

import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";

const isPublicRoute = createRouteMatcher(["/", "/sign-in(.*)", "/sign-up(.*)", "/api/webhooks(.*)"]);

export default clerkMiddleware(async (auth, req) => {
  if (!isPublicRoute(req)) {
    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)(.*)",
    "/__clerk/(.*)", // required for Clerk's own frontend API routes
  ],
};

app/dashboard/page.tsx (Protected route):

import { auth, currentUser } from "@clerk/nextjs/server";

export default async function DashboardPage() {
  const { userId } = await auth();
  const user = await currentUser();

  return (
    <div>
      <h1>Welcome, {user?.firstName}!</h1>
      <p>User ID: {userId}</p>
    </div>
  );
}

Client UI components

Clerk ships prebuilt components so you never hand-roll auth UI. <ClerkProvider> wraps the app and supplies auth context; <Show when="signed-in"> / <Show when="signed-out"> conditionally render based on session state; <UserButton> is the account menu/avatar; <SignInButton> / <SignUpButton> open the flows; and the <SignIn> / <SignUp> widgets mount on dedicated catch-all routes. In Clerk Core 3 (@clerk/nextjs v7) the old <SignedIn> / <SignedOut> / <Protect> control components have been removed entirely — rendering them now throws. Consolidate onto <Show>: map <SignedIn> → <Show when="signed-in">, <SignedOut> → <Show when="signed-out">, and <Protect role="…"> → <Show when={{ role: "…" }}> (import Show from the same package). <Show> also takes a fallback prop for the else branch.

app/layout.tsx (provider inside <body> + conditional header):

import {
  ClerkProvider,
  Show,
  SignInButton,
  SignUpButton,
  UserButton,
} from "@clerk/nextjs";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ClerkProvider>
          <header style={{ display: "flex", justifyContent: "flex-end", gap: 12, padding: 16 }}>
            <Show when="signed-out">
              <SignInButton />
              <SignUpButton />
            </Show>
            <Show when="signed-in">
              <UserButton />
            </Show>
          </header>
          {children}
        </ClerkProvider>
      </body>
    </html>
  );
}

app/sign-in/[[...sign-in]]/page.tsx (catch-all sign-in route):

import { SignIn } from "@clerk/nextjs";

export default function SignInPage() {
  return <SignIn />;
}

app/sign-up/[[...sign-up]]/page.tsx (catch-all sign-up route):

import { SignUp } from "@clerk/nextjs";

export default function SignUpPage() {
  return <SignUp />;
}

The component visibility maps to the middleware decision:

flowchart TD
  A["Page renders inside ClerkProvider"] --> B{"Session present?"}
  B -->|"yes"| C["Show when signed-in<br/>renders UserButton"]
  B -->|"no"| D["Show when signed-out<br/>renders SignInButton · SignUpButton"]
  D --> E["User clicks SignInButton"]
  E --> F["Catch-all route mounts SignIn widget"]

Gotcha: The [[...sign-in]] double-bracket optional catch-all is required — the widget handles sub-paths like /sign-in/factor-one and /sign-in/sso-callback internally. A plain page.tsx (no catch-all) breaks multi-factor and OAuth callback steps. Also make sure these routes stay public in middleware.ts (the createRouteMatcher example above already lists /sign-in(.*) and /sign-up(.*)).

API Route Protection

app/api/protected/route.ts:

import { auth } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";

export async function GET() {
  const { userId, orgId } = await auth();

  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  return NextResponse.json({ userId, orgId, message: "Protected data" });
}

Node.js / Express Backend

npm install @clerk/express
import express from "express";
import { clerkMiddleware, getAuth } from "@clerk/express";

const app = express();
app.use(clerkMiddleware());

// Recommended: clerkMiddleware() + getAuth(req). `requireAuth()` still exists
// but is deprecated — check `isAuthenticated` yourself instead.
app.get("/api/profile", (req, res) => {
  const { isAuthenticated, userId } = getAuth(req);
  if (!isAuthenticated) {
    return res.status(401).json({ error: "Unauthorized" });
  }
  res.json({ userId });
});

Backend SDK (Server-to-Server)

npm install @clerk/backend
import { createClerkClient } from "@clerk/backend";

const clerkClient = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY });

// List users
const { data: users } = await clerkClient.users.getUserList({ limit: 10 });

// Get a specific user
const user = await clerkClient.users.getUser(userId);

// Update user metadata
await clerkClient.users.updateUserMetadata(userId, {
  publicMetadata: { plan: "pro" },
  privateMetadata: { stripeCustomerId: "cus_..." },
});

// Delete a user
await clerkClient.users.deleteUser(userId);

Webhook Integration

This is the trust boundary that keeps your data safe — verify first, then act. The sequence below mirrors the handler code that follows.

sequenceDiagram
  participant C as "Clerk"
  participant R as "Webhook route"
  participant S as "Svix verify"
  participant DB as "Database"
  C->>R: "POST event with svix headers"
  R->>S: "Verify body and signature"
  alt valid signature
    S-->>R: "Verified event"
    R->>DB: "Apply user.created or user.deleted"
    R-->>C: "200 OK"
  else invalid
    S-->>R: "Throws"
    R-->>C: "400 Invalid signature"
  end

Setup Clerk Webhooks

Simpler path: verifyWebhook(req) from @clerk/nextjs/webhooks wraps Svix internally and reads CLERK_WEBHOOK_SIGNING_SECRET for you — no manual svix install or header plumbing. See PATTERNS.md and examples/webhook.ts. The raw-Svix flow below shows the mechanism underneath and stays useful in non-Next.js runtimes.

npm install svix  # only needed for the raw-Svix flow below

app/api/webhooks/clerk/route.ts:

import { Webhook } from "svix";
import { headers } from "next/headers";
import type { WebhookEvent } from "@clerk/nextjs/server";

export async function POST(req: Request) {
  const body = await req.text();
  const headerPayload = await headers();

  const wh = new Webhook(process.env.CLERK_WEBHOOK_SIGNING_SECRET!);
  let event: WebhookEvent;

  try {
    event = wh.verify(body, {
      "svix-id": headerPayload.get("svix-id")!,
      "svix-timestamp": headerPayload.get("svix-timestamp")!,
      "svix-signature": headerPayload.get("svix-signature")!,
    }) as WebhookEvent;
  } catch {
    return new Response("Invalid signature", { status: 400 });
  }

  switch (event.type) {
    case "user.created":
      await createUserInDatabase(event.data.id, event.data.email_addresses[0].email_address);
      break;
    case "user.deleted":
      await deleteUserFromDatabase(event.data.id!);
      break;
  }

  return new Response(null, { status: 200 });
}

Environment Variables

# Public (safe to expose in frontend)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding

# Secret (server-side only — NEVER expose in frontend)
CLERK_SECRET_KEY=sk_live_...
CLERK_WEBHOOK_SIGNING_SECRET=whsec_...

Automation Workflows

Claude Code Slash Command: Scaffold Auth

.claude/commands/clerk-auth.md:

Scaffold Clerk authentication for a Next.js App Router project.

Use the Clerk MCP server to get the latest implementation patterns, then:
1. Install `@clerk/nextjs` if not already in package.json
2. Create/update `middleware.ts` with `clerkMiddleware` and public routes
3. Wrap `app/layout.tsx` with `<ClerkProvider>`
4. Create `app/(auth)/sign-in/[[...sign-in]]/page.tsx` with `<SignIn>`
5. Create `app/(auth)/sign-up/[[...sign-up]]/page.tsx` with `<SignUp>`
6. Create `app/api/webhooks/clerk/route.ts` with user.created/deleted handlers
7. Add all required env vars to `.env.local`
8. Report what was created and any manual steps needed (webhook secret setup)

Usage: /project:clerk-auth

GitHub Actions: User Sync CI

# .github/workflows/clerk-sync.yml
name: Verify Clerk Config
on: [pull_request]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '22' }
      - run: npm ci
      - name: Check middleware exists
        run: test -f middleware.ts || (echo "Missing middleware.ts!" && exit 1)
      - name: Check env vars documented
        run: grep -q "CLERK_SECRET_KEY" .env.example || echo "Warning: CLERK_SECRET_KEY missing from .env.example"

Common Use Cases

Use Case Approach
Add auth to Next.js /project:clerk-auth slash command
Protect API routes await auth() (Next.js) / getAuth(req) + isAuthenticated (Express)
User metadata clerkClient.users.updateUserMetadata()
Sync users to DB Clerk webhook → user.created event
RBAC / permissions Clerk Organizations + auth().orgRole
MFA enforcement Clerk dashboard → Security settings

Troubleshooting

Issue Fix
401 on API route Ensure clerkMiddleware() is applied before route handler
Webhook signature fails Check CLERK_WEBHOOK_SIGNING_SECRET matches the Svix signing secret in the dashboard (env var renamed from CLERK_WEBHOOK_SECRET)
User not found after creation Webhook may have a delay; use clerkClient.users.getUser() to verify
Missing publishable key Check NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY in .env.local
Session not persisting Ensure <ClerkProvider> wraps the entire app layout

Official docs: