✓ Verified 💻 Development ✓ Enhanced Data

Sr Next Clerk Expert

Senior-level Clerk authentication expertise for Next.js 15/16+ applications.

Rating
4.2 (225 reviews)
Downloads
15,153 downloads
Version
1.0.0

Overview

Senior-level Clerk authentication expertise for Next.js 15/16+ applications.

Complete Documentation

View Source →

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 / for conditional contentServer errors on static pages
VWrap Clerk components in Flash of wrong content
VIPair with 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

text
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

typescript
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

tsx
// 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)

tsx
// 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

tsx
// 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

bash
# .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)

tsx
// ❌ 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

bash
# 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

  • Create new key in Clerk Dashboard
  • Update production env var (Vercel: vercel env rm CLERK_SECRET_KEY production && vercel env add CLERK_SECRET_KEY production)
  • Redeploy
  • Verify auth works
  • 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:
typescript
// ❌ 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
  • [ ] 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

Installation

Terminal bash

openclaw install sr-next-clerk-expert
    
Copied!

💻Code Examples

### Project Structure

-project-structure.txt
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

-the-only-correct-proxyts.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)(.*)",
  ],
};

### Public Page with Auth-Conditional Content

-public-page-with-auth-conditional-content.tsx
// 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)

-private-layout-route-protection.tsx
// 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

-root-layout-with-clerkprovider.tsx
// 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>
  );
}

NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard

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

---

## Common Errors & Fixes

| Error | Cause | Fix |
|-------|-------|-----|
| `MIDDLEWARE_INVOCATION_FAILED` | Missing/wrong CLERK_SECRET_KEY | Re-copy from dashboard |
| `?__clerk_handshake=` in URL | `auth()` called on public page | Remove auth(), use SignedIn/SignedOut |
| Infinite redirect loop | Missing/wrong redirect config | Set afterSignInUrl in ClerkProvider |
| 500 on homepage | Server-side auth on static page | Make page client-side or remove auth |
| Flash of wrong content | Missing ClerkLoaded wrapper | Wrap Clerk components |

---

## Anti-Patterns (NEVER DO)

// File: middleware.ts // DEPRECATED - USE proxy.ts

-file-middlewarets--deprecated---use-proxyts.txt
---

## Migration: middleware.ts → proxy.ts

npx @next/codemod@latest middleware-to-proxy

npx-nextcodemodlatest-middleware-to-proxy.txt
---

## 🔐 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:**
example.sh
# .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
example.txt
// ❌ 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

Tags

#web_and-frontend-development

Quick Info

Category Development
Model Claude 3.5
Complexity One-Click
Author michaelmonetized
Last Updated 3/10/2026
🚀
Optimized for
Claude 3.5
🧠

Ready to Install?

Get started with this skill in seconds

openclaw install sr-next-clerk-expert