{"slug":"clerk-authentication","title":"Clerk Authentication: Setup, Middleware, Webhooks, and User Management","tags":["clerk","authentication","nextjs","middleware","webhooks","users"],"agent_summary":"Clerk authentication integration for Next.js — installation, middleware configuration, auth in Server Components and API routes, user metadata, webhook sync to database, and production gotchas including auto-blocking behavior.","trigger_phrases":["Clerk","Clerk auth","Clerk middleware","Clerk Next.js","Clerk webhook","Clerk user","ClerkProvider","auth() Clerk","currentUser Clerk"],"runnable":false,"markdown":"\n## Overview\n\nClerk provides authentication with minimal setup for Next.js apps. Key gotcha: `@clerk/nextjs` middleware auto-blocks API routes by default — configure matcher carefully or remove if routes don't need auth.\n\n## Installation\n\n```bash\nnpm install @clerk/nextjs\n```\n\n```typescript\n// src/app/layout.tsx\nimport { ClerkProvider } from \"@clerk/nextjs\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <ClerkProvider>\n      <html lang=\"en\">\n        <body>{children}</body>\n      </html>\n    </ClerkProvider>\n  );\n}\n```\n\n## Environment Variables\n\n```bash\nNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...\nCLERK_SECRET_KEY=sk_test_...\n\n# Optional: customize redirect URLs\nNEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in\nNEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up\nNEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard\nNEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard\n```\n\n## Middleware Configuration\n\n```typescript\n// middleware.ts\nimport { clerkMiddleware, createRouteMatcher } from \"@clerk/nextjs/server\";\n\nconst isPublicRoute = createRouteMatcher([\n  \"/\",\n  \"/sign-in(.*)\",\n  \"/sign-up(.*)\",\n  \"/api/stripe/webhook\",  // must be public — Stripe can't authenticate\n  \"/api/health\",\n]);\n\nexport default clerkMiddleware((auth, request) => {\n  if (!isPublicRoute(request)) {\n    auth().protect();\n  }\n});\n\nexport const config = {\n  matcher: [\n    \"/((?!_next|[^?]*\\\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)\",\n    \"/(api|trpc)(.*)\",\n  ],\n};\n```\n\n**Critical**: If an API route does not need auth (public webhooks, health checks), add it to `isPublicRoute`. Without this, Clerk returns 401 before your handler runs.\n\n## Auth in Server Components\n\n```typescript\nimport { auth, currentUser } from \"@clerk/nextjs/server\";\n\n// Just get user ID (faster)\nexport default async function Dashboard() {\n  const { userId } = await auth();\n  if (!userId) redirect(\"/sign-in\");\n\n  const data = await fetchUserData(userId);\n  return <DashboardUI data={data} />;\n}\n\n// Get full user object\nexport default async function Profile() {\n  const user = await currentUser();\n  if (!user) redirect(\"/sign-in\");\n\n  return (\n    <div>\n      <h1>Welcome, {user.firstName}</h1>\n      <p>{user.emailAddresses[0]?.emailAddress}</p>\n    </div>\n  );\n}\n```\n\n## Auth in API Routes\n\n```typescript\nimport { auth } from \"@clerk/nextjs/server\";\n\nexport async function GET() {\n  const { userId } = await auth();\n  if (!userId) {\n    return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n  }\n\n  // Use userId as foreign key in your database\n  const projects = await db.from(\"projects\").select().eq(\"user_id\", userId);\n  return NextResponse.json({ projects });\n}\n```\n\n## Auth in Client Components\n\n```typescript\n\"use client\";\nimport { useAuth, useUser } from \"@clerk/nextjs\";\n\nexport function UserMenu() {\n  const { isLoaded, userId, isSignedIn } = useAuth();\n  const { user } = useUser();\n\n  if (!isLoaded) return <Spinner />;\n  if (!isSignedIn) return <SignInButton />;\n\n  return (\n    <div>\n      <span>{user?.firstName}</span>\n      <UserButton />  {/* Clerk's pre-built avatar dropdown */}\n    </div>\n  );\n}\n```\n\n## Pre-built UI Components\n\n```typescript\nimport {\n  SignIn,\n  SignUp,\n  UserButton,\n  SignInButton,\n  SignUpButton,\n  SignedIn,\n  SignedOut,\n} from \"@clerk/nextjs\";\n\n// Full-page sign-in\n// src/app/sign-in/[[...sign-in]]/page.tsx\nexport default function SignInPage() {\n  return <SignIn />;\n}\n\n// Conditional rendering\nfunction Header() {\n  return (\n    <nav>\n      <SignedOut>\n        <SignInButton>Login</SignInButton>\n      </SignedOut>\n      <SignedIn>\n        <UserButton />\n      </SignedIn>\n    </nav>\n  );\n}\n```\n\n## Webhook: Sync Users to Database\n\nWhen a user signs up or updates their profile, Clerk sends a webhook. Sync this to your database:\n\n```typescript\n// src/app/api/clerk/webhook/route.ts\nimport { Webhook } from \"svix\";\nimport { WebhookEvent } from \"@clerk/nextjs/server\";\n\nconst WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET!;\n\nexport async function POST(request: NextRequest) {\n  const body = await request.text();\n  const headers = {\n    \"svix-id\": request.headers.get(\"svix-id\")!,\n    \"svix-timestamp\": request.headers.get(\"svix-timestamp\")!,\n    \"svix-signature\": request.headers.get(\"svix-signature\")!,\n  };\n\n  let event: WebhookEvent;\n  try {\n    event = new Webhook(WEBHOOK_SECRET).verify(body, headers) as WebhookEvent;\n  } catch {\n    return NextResponse.json({ error: \"Invalid signature\" }, { status: 400 });\n  }\n\n  switch (event.type) {\n    case \"user.created\":\n    case \"user.updated\": {\n      const { id, email_addresses, first_name, last_name } = event.data;\n      await db.from(\"users\").upsert({\n        clerk_id: id,\n        email: email_addresses[0]?.email_address ?? \"\",\n        first_name: first_name ?? \"\",\n        last_name: last_name ?? \"\",\n      });\n      break;\n    }\n    case \"user.deleted\": {\n      await db.from(\"users\").delete().eq(\"clerk_id\", event.data.id);\n      break;\n    }\n  }\n\n  return NextResponse.json({ received: true });\n}\n```\n\nIn Clerk Dashboard → Webhooks, add endpoint `https://yourdomain.com/api/clerk/webhook` and subscribe to `user.created`, `user.updated`, `user.deleted`.\n\n## User Metadata\n\n```typescript\n// Set private metadata (server-side only)\nimport { clerkClient } from \"@clerk/nextjs/server\";\n\nawait (await clerkClient()).users.updateUserMetadata(userId, {\n  privateMetadata: {\n    stripeCustomerId: \"cus_xxx\",\n    subscriptionStatus: \"active\",\n  },\n  publicMetadata: {\n    plan: \"pro\",\n  },\n});\n\n// Read in server component\nconst user = await currentUser();\nconst plan = user?.publicMetadata.plan;\n```\n\n## Protecting Pages (Route Groups)\n\n```\napp/\n  (public)/\n    page.tsx        # marketing page — no auth\n  (authenticated)/\n    layout.tsx      # checks auth for all children\n    dashboard/\n      page.tsx\n    settings/\n      page.tsx\n```\n\n```typescript\n// app/(authenticated)/layout.tsx\nimport { auth } from \"@clerk/nextjs/server\";\nimport { redirect } from \"next/navigation\";\n\nexport default async function AuthLayout({ children }) {\n  const { userId } = await auth();\n  if (!userId) redirect(\"/sign-in\");\n  return <>{children}</>;\n}\n```\n","html":"<h2>Overview</h2>\n<p>Clerk provides authentication with minimal setup for Next.js apps. Key gotcha: <code>@clerk/nextjs</code> middleware auto-blocks API routes by default — configure matcher carefully or remove if routes don't need auth.</p>\n<h2>Installation</h2>\n<pre><code class=\"language-bash\">npm install @clerk/nextjs\n</code></pre>\n<pre><code class=\"language-typescript\">// src/app/layout.tsx\nimport { ClerkProvider } from \"@clerk/nextjs\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    &#x3C;ClerkProvider>\n      &#x3C;html lang=\"en\">\n        &#x3C;body>{children}&#x3C;/body>\n      &#x3C;/html>\n    &#x3C;/ClerkProvider>\n  );\n}\n</code></pre>\n<h2>Environment Variables</h2>\n<pre><code class=\"language-bash\">NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...\nCLERK_SECRET_KEY=sk_test_...\n\n# Optional: customize redirect URLs\nNEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in\nNEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up\nNEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard\nNEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard\n</code></pre>\n<h2>Middleware Configuration</h2>\n<pre><code class=\"language-typescript\">// middleware.ts\nimport { clerkMiddleware, createRouteMatcher } from \"@clerk/nextjs/server\";\n\nconst isPublicRoute = createRouteMatcher([\n  \"/\",\n  \"/sign-in(.*)\",\n  \"/sign-up(.*)\",\n  \"/api/stripe/webhook\",  // must be public — Stripe can't authenticate\n  \"/api/health\",\n]);\n\nexport default clerkMiddleware((auth, request) => {\n  if (!isPublicRoute(request)) {\n    auth().protect();\n  }\n});\n\nexport const config = {\n  matcher: [\n    \"/((?!_next|[^?]*\\\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)\",\n    \"/(api|trpc)(.*)\",\n  ],\n};\n</code></pre>\n<p><strong>Critical</strong>: If an API route does not need auth (public webhooks, health checks), add it to <code>isPublicRoute</code>. Without this, Clerk returns 401 before your handler runs.</p>\n<h2>Auth in Server Components</h2>\n<pre><code class=\"language-typescript\">import { auth, currentUser } from \"@clerk/nextjs/server\";\n\n// Just get user ID (faster)\nexport default async function Dashboard() {\n  const { userId } = await auth();\n  if (!userId) redirect(\"/sign-in\");\n\n  const data = await fetchUserData(userId);\n  return &#x3C;DashboardUI data={data} />;\n}\n\n// Get full user object\nexport default async function Profile() {\n  const user = await currentUser();\n  if (!user) redirect(\"/sign-in\");\n\n  return (\n    &#x3C;div>\n      &#x3C;h1>Welcome, {user.firstName}&#x3C;/h1>\n      &#x3C;p>{user.emailAddresses[0]?.emailAddress}&#x3C;/p>\n    &#x3C;/div>\n  );\n}\n</code></pre>\n<h2>Auth in API Routes</h2>\n<pre><code class=\"language-typescript\">import { auth } from \"@clerk/nextjs/server\";\n\nexport async function GET() {\n  const { userId } = await auth();\n  if (!userId) {\n    return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n  }\n\n  // Use userId as foreign key in your database\n  const projects = await db.from(\"projects\").select().eq(\"user_id\", userId);\n  return NextResponse.json({ projects });\n}\n</code></pre>\n<h2>Auth in Client Components</h2>\n<pre><code class=\"language-typescript\">\"use client\";\nimport { useAuth, useUser } from \"@clerk/nextjs\";\n\nexport function UserMenu() {\n  const { isLoaded, userId, isSignedIn } = useAuth();\n  const { user } = useUser();\n\n  if (!isLoaded) return &#x3C;Spinner />;\n  if (!isSignedIn) return &#x3C;SignInButton />;\n\n  return (\n    &#x3C;div>\n      &#x3C;span>{user?.firstName}&#x3C;/span>\n      &#x3C;UserButton />  {/* Clerk's pre-built avatar dropdown */}\n    &#x3C;/div>\n  );\n}\n</code></pre>\n<h2>Pre-built UI Components</h2>\n<pre><code class=\"language-typescript\">import {\n  SignIn,\n  SignUp,\n  UserButton,\n  SignInButton,\n  SignUpButton,\n  SignedIn,\n  SignedOut,\n} from \"@clerk/nextjs\";\n\n// Full-page sign-in\n// src/app/sign-in/[[...sign-in]]/page.tsx\nexport default function SignInPage() {\n  return &#x3C;SignIn />;\n}\n\n// Conditional rendering\nfunction Header() {\n  return (\n    &#x3C;nav>\n      &#x3C;SignedOut>\n        &#x3C;SignInButton>Login&#x3C;/SignInButton>\n      &#x3C;/SignedOut>\n      &#x3C;SignedIn>\n        &#x3C;UserButton />\n      &#x3C;/SignedIn>\n    &#x3C;/nav>\n  );\n}\n</code></pre>\n<h2>Webhook: Sync Users to Database</h2>\n<p>When a user signs up or updates their profile, Clerk sends a webhook. Sync this to your database:</p>\n<pre><code class=\"language-typescript\">// src/app/api/clerk/webhook/route.ts\nimport { Webhook } from \"svix\";\nimport { WebhookEvent } from \"@clerk/nextjs/server\";\n\nconst WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET!;\n\nexport async function POST(request: NextRequest) {\n  const body = await request.text();\n  const headers = {\n    \"svix-id\": request.headers.get(\"svix-id\")!,\n    \"svix-timestamp\": request.headers.get(\"svix-timestamp\")!,\n    \"svix-signature\": request.headers.get(\"svix-signature\")!,\n  };\n\n  let event: WebhookEvent;\n  try {\n    event = new Webhook(WEBHOOK_SECRET).verify(body, headers) as WebhookEvent;\n  } catch {\n    return NextResponse.json({ error: \"Invalid signature\" }, { status: 400 });\n  }\n\n  switch (event.type) {\n    case \"user.created\":\n    case \"user.updated\": {\n      const { id, email_addresses, first_name, last_name } = event.data;\n      await db.from(\"users\").upsert({\n        clerk_id: id,\n        email: email_addresses[0]?.email_address ?? \"\",\n        first_name: first_name ?? \"\",\n        last_name: last_name ?? \"\",\n      });\n      break;\n    }\n    case \"user.deleted\": {\n      await db.from(\"users\").delete().eq(\"clerk_id\", event.data.id);\n      break;\n    }\n  }\n\n  return NextResponse.json({ received: true });\n}\n</code></pre>\n<p>In Clerk Dashboard → Webhooks, add endpoint <code>https://yourdomain.com/api/clerk/webhook</code> and subscribe to <code>user.created</code>, <code>user.updated</code>, <code>user.deleted</code>.</p>\n<h2>User Metadata</h2>\n<pre><code class=\"language-typescript\">// Set private metadata (server-side only)\nimport { clerkClient } from \"@clerk/nextjs/server\";\n\nawait (await clerkClient()).users.updateUserMetadata(userId, {\n  privateMetadata: {\n    stripeCustomerId: \"cus_xxx\",\n    subscriptionStatus: \"active\",\n  },\n  publicMetadata: {\n    plan: \"pro\",\n  },\n});\n\n// Read in server component\nconst user = await currentUser();\nconst plan = user?.publicMetadata.plan;\n</code></pre>\n<h2>Protecting Pages (Route Groups)</h2>\n<pre><code>app/\n  (public)/\n    page.tsx        # marketing page — no auth\n  (authenticated)/\n    layout.tsx      # checks auth for all children\n    dashboard/\n      page.tsx\n    settings/\n      page.tsx\n</code></pre>\n<pre><code class=\"language-typescript\">// app/(authenticated)/layout.tsx\nimport { auth } from \"@clerk/nextjs/server\";\nimport { redirect } from \"next/navigation\";\n\nexport default async function AuthLayout({ children }) {\n  const { userId } = await auth();\n  if (!userId) redirect(\"/sign-in\");\n  return &#x3C;>{children}&#x3C;/>;\n}\n</code></pre>\n"}