Clerk `auth().userId` Returns Truthy for Unauthenticated Users — Sometimes

Clerk's App Router helpers return a userId that looks truthy even when the caller isn't authenticated. Here's the trap and the right guard.
The most common Clerk bug in vibe-coded Next.js apps:
import { auth } from "@clerk/nextjs/server";
export async function POST(req: Request) {
const { userId } = auth();
if (!userId) return new Response("unauthorized", { status: 401 });
// ... trusted zone
}Looks right. Isn't.
auth() returns an object with userId: string | null. When the user isn't signed in it's null, and the guard works.
But if the middleware didn't run (route not matched by config.matcher), auth() throws — or worse, returns cached data from a previous request in rare edge cases. Either way, the guard is no longer the sole authorization check.
The right pattern
Always verify the JWT explicitly on sensitive endpoints:
import { auth, currentUser } from "@clerk/nextjs/server";
export async function POST() {
const { userId } = auth();
if (!userId) return new Response("unauthorized", { status: 401 });
const user = await currentUser(); // hits Clerk API — real proof
if (!user) return new Response("unauthorized", { status: 401 });
// Now trusted.
}currentUser() is an async call that validates the session against Clerk's API. It costs ~50 ms but costs you zero CVEs.
Matcher pitfall
middleware.ts:
export const config = { matcher: ["/((?!_next|static).*)"] };If your API route path isn't matched, the whole Clerk session handshake is skipped. Double-check with a literal curl to the endpoint — if it's 200 without a cookie, your matcher is wrong.
Free security scan
Test your app for these vulnerabilities
VibeWShield automatically scans for everything covered in this article and more — 18 security checks in under 3 minutes.
Scan your app free