Sr Next Clerk Expert

Senior-level Clerk authentication expertise for Next.js 15/16+ applications. Use when implementing auth, protecting routes, fixing auth errors (500s, handshake redirects, middleware failures), integrating with Convex/Stripe, or debugging Clerk issues. Covers proxy.ts patterns, route groups, client vs server auth, and the 12 Commandments that prevent common disasters.

Audits

Pending

Install

openclaw skills install sr-next-clerk-expert

Senior Next.js + Clerk Expert

You are a senior engineer implementing Clerk authentication. Follow these patterns exactly—deviations cause production outages.


⚠️ CRITICAL: THE TWELVE COMMANDMENTS

These rules are non-negotiable. Violations cause 500 errors, infinite redirects, and broken sites.

#CommandmentViolation Consequence
IUse app/(private)/ route groupsMaintenance hell, broken auth
IIKeep proxy.ts simple (protect only private)Every new page needs proxy update
IIINEVER call auth() on public pages500 errors, slow pages, SEO death
IVUse <SignedIn>/<SignedOut> for conditional contentServer errors on static pages
VWrap Clerk components in <ClerkLoaded>Flash of wrong content
VIPair <ClerkLoaded> with <ClerkLoading>Jarring loading states
VIIConfigure redirects in ClerkProviderRedirect loops
VIIINo handshake redirects on public pagesBroken user experience
IXKeep marketing pages STATICSlow pages, bad SEO
XVerify env vars EXACTLY (copy-paste only)Cryptic 500 errors
XIUse proxy.ts not middleware.ts (Next.js 16+)Deprecation warnings
XIITest as anonymous user before deployShip broken auth

Quick Reference

Project Structure

app/
├── (private)/           # Protected - requires auth
│   ├── dashboard/
│   ├── settings/
│   └── layout.tsx       # Can call auth() here
├── page.tsx             # PUBLIC - NO auth()
├── layout.tsx           # Root - ClerkProvider
├── sign-in/[[...sign-in]]/page.tsx
└── sign-up/[[...sign-up]]/page.tsx

The ONLY Correct proxy.ts

import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";

const isPrivateRoute = createRouteMatcher(["/(private)(.*)"]);

export default clerkMiddleware(async (auth, request) => {
  if (isPrivateRoute(request)) {
    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)(.*)",
  ],
};

Patterns by Use Case

Public Page with Auth-Conditional Content

// app/page.tsx - CORRECT
import { ClerkLoaded, ClerkLoading, SignedIn, SignedOut } from "@clerk/nextjs";

export default function HomePage() {
  return (
    <main>
      <h1>Welcome</h1>
      <ClerkLoading>
        <Skeleton />
      </ClerkLoading>
      <ClerkLoaded>
        <SignedOut>
          <a href="/sign-in">Sign In</a>
        </SignedOut>
        <SignedIn>
          <a href="/dashboard">Dashboard</a>
        </SignedIn>
      </ClerkLoaded>
    </main>
  );
}

Private Layout (Route Protection)

// app/(private)/layout.tsx
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";

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

Root Layout with ClerkProvider

// app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider
      signInUrl="/sign-in"
      signUpUrl="/sign-up"
      afterSignInUrl="/dashboard"
      afterSignUpUrl="/dashboard"
    >
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}

Advanced Patterns

For complex integrations, see reference files:


Environment Variables

# .env.local - COPY FROM CLERK DASHBOARD (do not type manually)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...

# Optional redirects
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=/dashboard

⚠️ CRITICAL: Copy-paste keys from Clerk dashboard. Manual typing causes 1/l and x/X errors that produce cryptic 500s.


Common Errors & Fixes

ErrorCauseFix
MIDDLEWARE_INVOCATION_FAILEDMissing/wrong CLERK_SECRET_KEYRe-copy from dashboard
?__clerk_handshake= in URLauth() called on public pageRemove auth(), use SignedIn/SignedOut
Infinite redirect loopMissing/wrong redirect configSet afterSignInUrl in ClerkProvider
500 on homepageServer-side auth on static pageMake page client-side or remove auth
Flash of wrong contentMissing ClerkLoaded wrapperWrap Clerk components

Anti-Patterns (NEVER DO)

// ❌ WRONG - auth() on public page
export default async function HomePage() {
  const { userId } = await auth();  // BREAKS STATIC RENDERING
  if (userId) redirect("/dashboard");
  return <LandingPage />;
}

// ❌ WRONG - listing every public route
const isPublicRoute = createRouteMatcher([
  "/", "/about", "/pricing", "/blog", "/contact", // MAINTENANCE HELL
]);

// ❌ WRONG - no ClerkLoaded wrapper
<SignedIn>
  <UserButton />  // FLASHES INCORRECTLY
</SignedIn>

// ❌ WRONG - middleware.ts in Next.js 16+
// File: middleware.ts  // DEPRECATED - USE proxy.ts

Migration: middleware.ts → proxy.ts

# Option 1: Rename
mv middleware.ts proxy.ts

# Option 2: Codemod
npx @next/codemod@latest middleware-to-proxy

🔐 Security Best Practices

Secret Management

  • Store secrets in platform env vars (Vercel, Railway, etc.) — never in code or git
  • Use separate keys for dev/staging/prod — Clerk provides different instances
  • Rotate keys if compromised — Clerk Dashboard → API Keys → Add new key → update env → delete old
  • Limit access — only team members who need keys should have dashboard access

Key Rotation Procedure

  1. Create new key in Clerk Dashboard
  2. Update production env var (Vercel: vercel env rm CLERK_SECRET_KEY production && vercel env add CLERK_SECRET_KEY production)
  3. Redeploy
  4. Verify auth works
  5. Delete old key from Clerk Dashboard

Webhook Security

  • Always verify signatures — use svix library (shown in references/webhooks.md)
  • Use HTTPS endpoints only — never expose webhook URLs over HTTP
  • Store CLERK_WEBHOOK_SECRET securely — same as other secrets

Debug Logging

⚠️ NEVER use debug mode in production:

// ❌ REMOVE BEFORE DEPLOYING
export default clerkMiddleware(
  async (auth, request) => { /* ... */ },
  { debug: true }  // LEAKS TOKENS TO LOGS
);

Debug mode logs handshake tokens (?__clerk_handshake=) which are sensitive. Use only in local development.

Least Privilege

SecretScopeNotes
CLERK_SECRET_KEYServer onlyNever expose to client
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYClient safeCan be in client bundles
CLERK_WEBHOOK_SECRETServer onlyWebhook handler only
STRIPE_SECRET_KEYServer onlyAPI routes only

Verification Checklist

Before deploying, verify:

  • proxy.ts exists (not middleware.ts)
  • proxy.ts ONLY protects /(private) routes
  • No auth() calls in app/page.tsx or marketing pages
  • All Clerk components wrapped in <ClerkLoaded>
  • <ClerkLoading> shows skeleton/spinner
  • Env vars copied exactly from Clerk dashboard
  • Anonymous user can access homepage (incognito test)
  • Sign-in redirects to correct page
  • Dashboard requires authentication