← Back to dashboard

Clerk Integration Guide

How does auth actually work?

The real model

Clerk-issued session JWTs with org-aware claims, gated by middleware.

The session JWT carries `sub` (user id), optionally `org_id`, `org_role`, `org_permissions`, plus any custom claims you map in the Clerk dashboard. The Next.js SDK adds a `/middleware.ts` that calls `clerkMiddleware()` — wrap routes in `await auth.protect()` to enforce sign-in, or `(await auth()).has({ role })` for fine-grained checks. On the server, `auth()` is async — you `await auth()` to read the verified claims; `currentUser()` is also async and round-trips to Clerk for the full profile. Webhooks (Svix-signed) keep your DB in sync on user.created / organization.updated / etc. — never trust the JWT alone for anything beyond the claims it carries.

Five Clerk primitives

Most of Clerk is User × Session × Organization. Roles and webhooks make those usable in production code.

Toggle the auth state, watch the routes open and close

The JWT on the left is what auth() sees on the server. The matrix on the right is what your middleware.ts would decide for each route. Flip the toggles and see how three booleans cover almost every B2B-SaaS access rule you'd write.

Auth state explorersigned in · org:member
Roleorg_role claim
auth() claims server-side
{
  sub: "user_2bD8Bx…",
  email: "ada@codeamani.com",
  iat: "1717084812",
  exp: "1717085412",
  org_id: "org_2bDhKp…",
  org_role: "org:member",
}
Route access (middleware decision)
✓/marketing sitepublic — no auth required
✓/dashboarduser dashboardrequires `auth().protect()`
✓/org-settingsteam settingsrequires an active organization
✗/adminadmin consolerequires `org:admin` role

Flip In an organization off and watch /org-settings + /admin close — the middleware reads only the JWT claims to decide, no database hit. Switch role to admin and the admin console opens; the JWT carries the new claim from the next request after setActive().

The unlock: (await auth()).has({ role }) is a JWT claim check — it doesn't hit a database, so route gating is essentially free at runtime, even on Edge SSR.

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

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


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.

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

.mcp.json Configuration

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

What the Clerk MCP Server Provides

CapabilityDescription
SDK snippetsUp-to-date @clerk/nextjs, @clerk/express, @clerk/backend examples
Component usage<SignIn>, <UserButton>, <ClerkProvider> patterns
Auth helpersauth(), currentUser(), getAuth() usage
Webhook setupSvix-verified webhook handler patterns
OAuth flowsSocial provider configuration examples
RBAC patternsRole 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.

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

Bash
# 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:

TypeScript
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:

TypeScript
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):

TypeScript
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):

TypeScript
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):

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

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

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

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

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

The component visibility maps to the middleware decision:

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:

TypeScript
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

Bash
npm install @clerk/express
TypeScript
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)

Bash
npm install @clerk/backend
TypeScript
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.

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.

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

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

TypeScript
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

Bash
# 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:

Markdown
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

YAML
# .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 CaseApproach
Add auth to Next.js/project:clerk-auth slash command
Protect API routesawait auth() (Next.js) / getAuth(req) + isAuthenticated (Express)
User metadataclerkClient.users.updateUserMetadata()
Sync users to DBClerk webhook → user.created event
RBAC / permissionsClerk Organizations + auth().orgRole
MFA enforcementClerk dashboard → Security settings

Troubleshooting

IssueFix
401 on API routeEnsure clerkMiddleware() is applied before route handler
Webhook signature failsCheck CLERK_WEBHOOK_SIGNING_SECRET matches the Svix signing secret in the dashboard (env var renamed from CLERK_WEBHOOK_SECRET)
User not found after creationWebhook may have a delay; use clerkClient.users.getUser() to verify
Missing publishable keyCheck NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY in .env.local
Session not persistingEnsure <ClerkProvider> wraps the entire app layout