Clerk Authentication Error? Fix Missing publishableKey and clerkMiddleware Issues
Troubleshoot the most common Clerk errors in Next.js and React apps — Missing publishableKey, auth() was called but Clerk can't detect usage of clerkMiddleware(), env var mixups, and the deprecated authMiddleware migration.
Common errors and fixes
1. "Clerk: Missing publishableKey"
Thrown by <ClerkProvider> when it cannot find NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY at render time. Usually the variable is missing, misspelled, or was added after the dev server started.
Error: @clerk/clerk-react: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys. # .env.local — must use NEXT_PUBLIC_ prefix, must exist locally
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxx
CLERK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxx - Restart the dev server: Next.js reads env files at process start — editing
.env.localwhile the server is running does nothing until you restart. - Wrong file:
.envis often committed and left blank for secrets — local overrides belong in.env.local, which is gitignored by default. - On Vercel/Netlify: set the variable per-environment (Production, Preview, Development) in the dashboard and trigger a new deployment — env changes do not apply to already-built deployments.
2. "auth() was called but Clerk can't detect usage of clerkMiddleware()"
This fires when a server component, route handler, or server action calls auth() for a request that never passed through Clerk's middleware — either middleware.ts is missing, misplaced, or its matcher excludes the route.
Error: auth() was called but Clerk can't detect usage of clerkMiddleware().
Please ensure the following:
- clerkMiddleware() is used in your Next.js Middleware.
- Your Middleware matcher is configured to match this route or page. Fix — correct middleware.ts placement and matcher:
// middleware.ts — project root, or src/middleware.ts if using a src/ dir
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/api/private(.*)']);
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect();
}
});
export const config = {
matcher: [
// Skip Next.js internals and static files, but ALWAYS run for API routes
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
'/(api|trpc)(.*)',
],
}; '/(api|trpc)(.*)' — API routes then skip the middleware entirely, and any auth() call inside them throws this error.
3. Publishable key vs secret key mixup
Pasting CLERK_SECRET_KEY where NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY belongs (or the reverse) produces confusing 401s or, worse, leaks the secret key into client bundles.
// app/layout.tsx — client-safe key ONLY
import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({ children }) {
return (
<ClerkProvider>
<html lang="en"><body>{children}</body></html>
</ClerkProvider>
);
}
// app/api/private/route.ts — secret key used implicitly server-side only
import { auth } from '@clerk/nextjs/server';
export async function GET() {
const { userId } = await auth();
if (!userId) return new Response('Unauthorized', { status: 401 });
return Response.json({ userId });
} - Rule of thumb: if it starts with
pk_, it goes in aNEXT_PUBLIC_variable. If it starts withsk_, it never gets that prefix. - If you leaked a secret key: rotate it immediately in the Clerk dashboard under API Keys — a leaked secret key allows full server-side impersonation.
4. Migrating from deprecated authMiddleware() to clerkMiddleware()
Clerk v5 deprecated authMiddleware() in favor of clerkMiddleware(). The old publicRoutes array is replaced by explicit route matching inside the middleware body.
// OLD (deprecated, Clerk v4)
import { authMiddleware } from '@clerk/nextjs';
export default authMiddleware({
publicRoutes: ['/', '/sign-in', '/api/webhooks(.*)'],
});
// NEW (Clerk v5+)
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher(['/', '/sign-in(.*)', '/api/webhooks(.*)']);
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect();
}
});
Also update every import from @clerk/nextjs to @clerk/nextjs/server for server-only helpers like auth(), currentUser(), and clerkClient().
5. useUser() / useAuth() returning null during SSR
On first render, Clerk hasn't loaded session state on the client yet, so useUser() returns { isLoaded: false, isSignedIn: undefined, user: null }. Rendering UI based on user before checking isLoaded causes a flash of signed-out content or hydration mismatches.
'use client';
import { useUser } from '@clerk/nextjs';
export function Greeting() {
const { isLoaded, isSignedIn, user } = useUser();
if (!isLoaded) return <Skeleton />; // avoid hydration mismatch
if (!isSignedIn) return <SignInPrompt />;
return <p>Welcome, {user.firstName}</p>;
} - Server components: prefer
auth()/currentUser()from@clerk/nextjs/server— they resolve synchronously on the server so there is no loading state to handle. - Session token expiry: Clerk auto-refreshes short-lived session tokens client-side via
ClerkProvider. For custom fetches to your own backend, callgetToken()fromuseAuth()right before the request rather than caching the token, since it expires roughly every 60 seconds.
6. Webhook signature verification failing for user.created
Clerk webhooks (e.g. user.created) are signed with Svix headers. Verification fails when the raw request body is parsed as JSON before verifying — the signature is computed over the exact raw bytes.
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
import { headers } from 'next/headers';
export async function POST(req: Request) {
const payload = await req.text(); // raw text, NOT req.json()
const headerPayload = await headers();
const wh = new Webhook(process.env.CLERK_WEBHOOK_SIGNING_SECRET);
const evt = wh.verify(payload, {
'svix-id': headerPayload.get('svix-id'),
'svix-timestamp': headerPayload.get('svix-timestamp'),
'svix-signature': headerPayload.get('svix-signature'),
});
if (evt.type === 'user.created') {
// sync evt.data to your database
}
return new Response('OK', { status: 200 });
} Not sure if it's your code or an outage?
Prismix tracks Clerk, Vercel, and other AI/dev service status in real time — free email alerts when something goes down, no credit card.
FAQ
How do I fix "Clerk: Missing publishableKey"?
Set NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY in .env.local, confirm the pk_ prefix, and restart the dev server. On hosting platforms, set it per-environment and redeploy.
Why does Clerk say "auth() was called but Clerk can't detect usage of clerkMiddleware()"?
Your middleware.ts is missing, misplaced, or its matcher config excludes the route calling auth() — commonly API routes under /api.
What is the difference between NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY?
The publishable key is safe for the browser and initializes ClerkProvider. The secret key must stay server-only — never prefix it with NEXT_PUBLIC_.
How do I migrate from authMiddleware() to clerkMiddleware()?
Import from @clerk/nextjs/server, rename authMiddleware to clerkMiddleware, and replace publicRoutes with createRouteMatcher() plus auth.protect().