Clerk + Claude Code Integration Guide
How does auth actually work?
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.
{
sub: "user_2bD8Bx…",
email: "ada@codeamani.com",
iat: "1717084812",
exp: "1717085412",
org_id: "org_2bDhKp…",
org_role: "org:member",
}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.
██████╗██╗ ███████╗██████╗ ██╗ ██╗
██╔════╝██║ ██╔════╝██╔══██╗██║ ██╔╝
██║ ██║ █████╗ ██████╔╝█████╔╝
██║ ██║ ██╔══╝ ██╔══██╗██╔═██╗
╚██████╗███████╗███████╗██║ ██║██║ ██╗
╚═════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝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
| 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.
npm install @clerk/nextjsapp/layout.tsx:
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:
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):
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):
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):
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:
Gotcha: The
[[...sign-in]]double-bracket optional catch-all is required — the widget handles sub-paths like/sign-in/factor-oneand/sign-in/sso-callbackinternally. A plainpage.tsx(no catch-all) breaks multi-factor and OAuth callback steps. Also make sure these routes stay public inmiddleware.ts(thecreateRouteMatcherexample 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/expressimport 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)
npm install @clerk/backendimport { 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
npm install svix # for webhook signature verificationapp/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_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_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 | requireAuth() middleware |
| 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_SECRET matches the Svix secret in dashboard |
| 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 |