Claude Code for Clerk: Authentication and User Management — Claude Skills 360 Blog
Blog / Backend / Claude Code for Clerk: Authentication and User Management
Backend

Claude Code for Clerk: Authentication and User Management

Published: April 13, 2027
Read time: 7 min read
By: Claude Skills 360

Clerk is a complete authentication and user management solution — <ClerkProvider> wraps the app with auth context. <SignIn> and <SignUp> are drop-in components. useUser() returns { user, isLoaded } in Client Components. useAuth() returns { userId, sessionId, getToken }. clerkMiddleware() in middleware.ts protects routes — auth().protect() throws 404 for unauthenticated. currentUser() gets the user in Server Components. auth() in Server Actions returns the session. <UserButton> shows avatar with sign-out. <OrganizationSwitcher> enables multi-tenant team switching. Clerk webhooks sync users to your database on user.created/updated/deleted events. user.publicMetadata stores custom role and plan data. JWT templates add custom claims to tokens. Claude Code generates Clerk middleware, protected layouts, webhook handlers, and role-based access patterns for Next.js App Router.

CLAUDE.md for Clerk

## Clerk Stack
- Version: @clerk/nextjs >= 5.0
- Provider: ClerkProvider wraps layout.tsx — no config needed if CLERK_* env vars set
- Middleware: clerkMiddleware() in middleware.ts — createRouteMatcher for public routes
- Server Component: const user = await currentUser() — null if unauthenticated
- Server Action: const { userId } = auth(); if (!userId) throw new Error("Unauthorized")
- Client: const { user, isLoaded } = useUser() — const { userId, getToken } = useAuth()
- Protect: auth().protect() — redirects to sign-in, or { unauthorizedUrl: "/403" }
- Metadata: await clerkClient().users.updateUser(userId, { publicMetadata: { role: "admin" } })
- Webhook: svix verify signature → handle user.created/updated/deleted events

App Setup

// app/layout.tsx — ClerkProvider wraps everything
import { ClerkProvider } from "@clerk/nextjs"
import { dark } from "@clerk/themes"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider
      appearance={{
        baseTheme: undefined,  // or dark for dark mode
        variables: {
          colorPrimary: "#3b82f6",
          colorBackground: "#ffffff",
          borderRadius: "0.5rem",
          fontFamily: "Inter, sans-serif",
        },
        elements: {
          formButtonPrimary: "bg-primary hover:bg-primary/90 text-primary-foreground",
          card: "shadow-sm border",
        },
      }}
    >
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  )
}
// middleware.ts — route protection
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"

// Define public routes that don't require authentication
const isPublicRoute = createRouteMatcher([
  "/",
  "/sign-in(.*)",
  "/sign-up(.*)",
  "/api/webhooks(.*)",
  "/blog(.*)",
  "/pricing",
])

export default clerkMiddleware(async (auth, request) => {
  // Protect all non-public routes
  if (!isPublicRoute(request)) {
    await auth.protect()
  }
})

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico).*)",
    "/(api|trpc)(.*)",
  ],
}

Auth Pages

// app/sign-in/[[...sign-in]]/page.tsx — catch-all sign-in page
import { SignIn } from "@clerk/nextjs"

export default function SignInPage() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-muted/50">
      <SignIn
        appearance={{
          elements: {
            rootBox: "mx-auto",
            card: "shadow-lg",
          },
        }}
        redirectUrl="/dashboard"
      />
    </div>
  )
}

// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from "@clerk/nextjs"

export default function SignUpPage() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-muted/50">
      <SignUp redirectUrl="/onboarding" />
    </div>
  )
}

Protected Layouts and Server Components

// app/dashboard/layout.tsx — protected layout
import { auth, currentUser } from "@clerk/nextjs/server"
import { redirect } from "next/navigation"
import { UserButton, OrganizationSwitcher } from "@clerk/nextjs"

export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
  const { userId } = await auth()
  if (!userId) redirect("/sign-in")

  const user = await currentUser()

  return (
    <div className="min-h-screen">
      <header className="border-b">
        <div className="container mx-auto flex items-center justify-between h-14 px-4">
          <div className="flex items-center gap-4">
            <span className="font-semibold">Dashboard</span>
            <OrganizationSwitcher
              appearance={{
                elements: {
                  organizationSwitcherTrigger: "border rounded px-2 py-1",
                },
              }}
            />
          </div>

          <div className="flex items-center gap-3">
            <span className="text-sm text-muted-foreground">
              {user?.firstName} {user?.lastName}
            </span>
            <UserButton
              afterSignOutUrl="/"
              appearance={{
                elements: { avatarBox: "size-8" },
              }}
            />
          </div>
        </div>
      </header>
      <main className="container mx-auto py-6 px-4">{children}</main>
    </div>
  )
}

// app/dashboard/page.tsx — Server Component with user data
import { currentUser, auth } from "@clerk/nextjs/server"
import { db } from "@/lib/db"

export default async function DashboardPage() {
  const clerkUser = await currentUser()
  if (!clerkUser) return null

  // Fetch from your own database using Clerk userId as foreign key
  const userRecord = await db.user.findUnique({
    where: { clerkId: clerkUser.id },
    include: { subscription: true },
  })

  const role = clerkUser.publicMetadata.role as string | undefined

  return (
    <div>
      <h1 className="text-2xl font-bold">Welcome back, {clerkUser.firstName}</h1>
      <p className="text-muted-foreground">{clerkUser.emailAddresses[0].emailAddress}</p>
      {role === "admin" && (
        <div className="mt-4 p-3 bg-yellow-50 rounded-lg text-sm">
          Admin access enabled
        </div>
      )}
    </div>
  )
}

Server Actions with Auth

// app/actions/post-actions.ts — protected Server Actions
"use server"
import { auth } from "@clerk/nextjs/server"
import { db } from "@/lib/db"
import { revalidatePath } from "next/cache"
import { z } from "zod"

const CreatePostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
  published: z.boolean().optional().default(false),
})

export async function createPost(formData: FormData) {
  const { userId } = await auth()
  if (!userId) throw new Error("Unauthorized")

  const input = CreatePostSchema.parse({
    title: formData.get("title"),
    content: formData.get("content"),
    published: formData.get("published") === "on",
  })

  const post = await db.post.create({
    data: { ...input, authorId: userId },
  })

  revalidatePath("/dashboard/posts")
  return { id: post.id }
}

export async function deletePost(postId: string) {
  const { userId } = await auth()
  if (!userId) throw new Error("Unauthorized")

  // Verify ownership
  const post = await db.post.findFirst({
    where: { id: postId, authorId: userId },
  })
  if (!post) throw new Error("Not found or forbidden")

  await db.post.delete({ where: { id: postId } })
  revalidatePath("/dashboard/posts")
}

// Admin-only action
export async function promoteToAdmin(targetUserId: string) {
  const { userId } = await auth()
  if (!userId) throw new Error("Unauthorized")

  // Check caller is admin via publicMetadata
  const { clerkClient } = await import("@clerk/nextjs/server")
  const callerUser = await clerkClient().users.getUser(userId)
  if (callerUser.publicMetadata.role !== "admin") {
    throw new Error("Forbidden: admin access required")
  }

  // Set role on target user
  await clerkClient().users.updateUser(targetUserId, {
    publicMetadata: { role: "admin" },
  })
}

Webhook Database Sync

// app/api/webhooks/clerk/route.ts — sync Clerk users to database
import { headers } from "next/headers"
import { Webhook } from "svix"
import { db } from "@/lib/db"
import type { WebhookEvent } from "@clerk/nextjs/server"

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

  const svix_id = headerPayload.get("svix-id")
  const svix_timestamp = headerPayload.get("svix-timestamp")
  const svix_signature = headerPayload.get("svix-signature")

  if (!svix_id || !svix_timestamp || !svix_signature) {
    return new Response("Missing svix headers", { status: 400 })
  }

  const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!)

  let event: WebhookEvent
  try {
    event = wh.verify(body, {
      "svix-id": svix_id,
      "svix-timestamp": svix_timestamp,
      "svix-signature": svix_signature,
    }) as WebhookEvent
  } catch {
    return new Response("Invalid signature", { status: 400 })
  }

  switch (event.type) {
    case "user.created": {
      const { id, email_addresses, first_name, last_name, image_url } = event.data
      await db.user.create({
        data: {
          clerkId: id,
          email: email_addresses[0].email_address,
          name: [first_name, last_name].filter(Boolean).join(" ") || "Anonymous",
          avatarUrl: image_url,
        },
      })
      break
    }

    case "user.updated": {
      const { id, email_addresses, first_name, last_name, image_url } = event.data
      await db.user.update({
        where: { clerkId: id },
        data: {
          email: email_addresses[0].email_address,
          name: [first_name, last_name].filter(Boolean).join(" ") || "Anonymous",
          avatarUrl: image_url,
        },
      })
      break
    }

    case "user.deleted": {
      if (event.data.id) {
        await db.user.delete({ where: { clerkId: event.data.id } })
      }
      break
    }
  }

  return new Response("OK", { status: 200 })
}

For the Better Auth alternative when a self-hosted, open-source TypeScript authentication library with Prisma/Drizzle adapters, email/password, OAuth, organizations, and two-factor is preferred — Better Auth runs entirely in your own infrastructure with no per-seat pricing, see the Better Auth guide. For the Auth.js (NextAuth) alternative when a battle-tested, community-driven authentication library with 50+ OAuth providers and official framework adapters is preferred — Auth.js v5 has a newer server-first API with auth() for both Server Components and middleware, see the NextAuth guide. The Claude Skills 360 bundle includes Clerk skill sets covering middleware, webhooks, and organization management. Start with the free tier to try auth integration generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free