← Back to dashboard
clerkauthreview due

Clerk + Claude Code 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 `auth.protect()` to enforce sign-in, or `auth().has({ role })` for fine-grained checks. On the server, `auth()` is sync and reads the verified claims; `currentUser()` is 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: 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 + Claude Code 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.

Bash
npm install @clerk/nextjs

app/layout.tsx:

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

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}

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)(.*)"],
};

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. As of Clerk Core 3, <Show> replaces the older <SignedIn> / <SignedOut> / <Protect> control components — map <SignedIn><Show when="signed-in"> and <SignedOut><Show when="signed-out"> (import Show from the same package).

app/layout.tsx (provider + conditional header):

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

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>
          <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}
        </body>
      </html>
    </ClerkProvider>
  );
}

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, requireAuth, getAuth } from "@clerk/express";

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

// Protect routes
app.get("/api/profile", requireAuth(), (req, res) => {
  const { userId } = getAuth(req);
  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

Bash
npm install svix  # for webhook signature verification

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_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_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 routesrequireAuth() middleware
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_SECRET matches the Svix secret in dashboard
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