You are a senior engineer implementing Clerk authentication. Follow these patterns exactly—deviations cause production outages.
These rules are non-negotiable. Violations cause 500 errors, infinite redirects, and broken sites.
#CommandmentViolation ConsequenceIUse app/(private)/ route groupsMaintenance hell, broken authIIKeep proxy.ts simple (protect only private)Every new page needs proxy updateIIINEVER call auth() on public pages500 errors, slow pages, SEO deathIVUse <SignedIn>/ <SignedOut> for conditional contentServer errors on static pagesVWrap Clerk components in <ClerkLoaded>Flash of wrong contentVIPair <ClerkLoaded> with <ClerkLoading>Jarring loading statesVIIConfigure redirects in ClerkProviderRedirect loopsVIIINo handshake redirects on public pagesBroken user experienceIXKeep marketing pages STATICSlow pages, bad SEOXVerify env vars EXACTLY (copy-paste only)Cryptic 500 errorsXIUse proxy.ts not middleware.ts (Next.js 16+)Deprecation warningsXIITest as anonymous user before deployShip broken auth
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
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)(.*)",
],
};
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>
);
}
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}</>;
}
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>
);
}
For complex integrations, see reference files:
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.
ErrorCauseFixMIDDLEWARE_INVOCATION_FAILEDMissing/wrong CLERK_SECRET_KEYRe-copy from dashboard?__clerk_handshake= in URLauth() called on public pageRemove auth(), use SignedIn/SignedOutInfinite redirect loopMissing/wrong redirect configSet afterSignInUrl in ClerkProvider500 on homepageServer-side auth on static pageMake page client-side or remove authFlash of wrong contentMissing ClerkLoaded wrapperWrap Clerk components
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
bash
# Option 1: Rename
mv middleware.ts proxy.ts
# Option 2: Codemod
npx @next/codemod@latest middleware-to-proxy
vercel env rm CLERK_SECRET_KEY production && vercel env add CLERK_SECRET_KEY production)svix library (shown in references/webhooks.md)⚠️ 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.
SecretScopeNotesCLERK_SECRET_KEYServer onlyNever expose to clientNEXT_PUBLIC_CLERK_PUBLISHABLE_KEYClient safeCan be in client bundlesCLERK_WEBHOOK_SECRETServer onlyWebhook handler onlySTRIPE_SECRET_KEYServer onlyAPI routes only
Before deploying, verify:
proxy.ts exists (not middleware.ts)/(private) routesauth() calls in app/page.tsx or marketing pages<ClerkLoaded><ClerkLoading> shows skeleton/spinner