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/nextjsv7):<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 aJOIN-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-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/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/webhookswraps Svix internally and readsCLERK_WEBHOOK_SIGNING_SECRETfor you — no manualsvixinstall or header plumbing. SeePATTERNS.mdandexamples/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: