Fix: Clerk Not Working — Auth Not Loading, Middleware Blocking, or User Data Missing
Quick Answer
How to fix Clerk authentication issues — ClerkProvider setup, middleware configuration, useUser and useAuth hooks, server-side auth, webhook handling, and organization features.
The Problem
Clerk components render but show a loading state forever:
import { SignInButton, UserButton } from '@clerk/nextjs';
function Header() {
return (
<div>
<UserButton /> {/* Spinner that never resolves */}
<SignInButton />
</div>
);
}Or middleware blocks every page, including the login page:
Redirect loop: /sign-in → middleware redirects to /sign-in → ...Or currentUser() returns null in a Server Component:
import { currentUser } from '@clerk/nextjs/server';
const user = await currentUser();
// null — even though the user is logged inWhy This Happens
Clerk is a managed authentication service. Your app communicates with Clerk’s servers for all auth operations:
ClerkProvidermust wrap the app — all Clerk hooks and components need the provider context. Without it, components render loading states indefinitely because they can’t access the Clerk instance.- Environment variables must be set —
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYandCLERK_SECRET_KEYare required. The publishable key (client-side) initializes the Clerk frontend. The secret key (server-side) authenticates API calls. - Middleware must exclude public routes — Clerk’s
clerkMiddleware()protects routes by default. If the sign-in page itself is protected, you get a redirect loop. Public routes must be explicitly defined. - Server-side auth uses the secret key —
currentUser()andauth()in Server Components communicate with Clerk’s API usingCLERK_SECRET_KEY. If the key is missing or invalid, server-side auth fails silently.
Fix 1: Setup with Next.js App Router
npm install @clerk/nextjs# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxx
CLERK_SECRET_KEY=sk_test_xxxxxxxx
# Custom routes (optional)
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up// app/layout.tsx — wrap with ClerkProvider
import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}// middleware.ts — protect routes
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
// Define public routes that don't require authentication
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/api/webhooks(.*)',
'/blog(.*)',
'/pricing',
]);
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect(); // Redirect to sign-in if not authenticated
}
});
export const config = {
matcher: [
// Skip static files and internals
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
'/(api|trpc)(.*)',
],
};Fix 2: Auth in Components
// Client component — hooks
'use client';
import { useUser, useAuth, useClerk, SignInButton, SignOutButton, UserButton } from '@clerk/nextjs';
function Header() {
const { isLoaded, isSignedIn, user } = useUser();
const { userId, sessionId, getToken } = useAuth();
if (!isLoaded) return <div>Loading...</div>;
if (!isSignedIn) {
return (
<div>
<SignInButton mode="modal">
<button>Sign In</button>
</SignInButton>
</div>
);
}
return (
<div>
<span>Welcome, {user.firstName}</span>
<UserButton afterSignOutUrl="/" />
</div>
);
}
// Get JWT for API calls
function ApiCaller() {
const { getToken } = useAuth();
async function callApi() {
const token = await getToken();
const res = await fetch('/api/protected', {
headers: { Authorization: `Bearer ${token}` },
});
return res.json();
}
return <button onClick={callApi}>Call API</button>;
}// Server Component — direct auth access
// app/dashboard/page.tsx
import { currentUser, auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const user = await currentUser();
if (!user) redirect('/sign-in');
return (
<div>
<h1>Dashboard</h1>
<p>Hello, {user.firstName} {user.lastName}</p>
<p>Email: {user.emailAddresses[0]?.emailAddress}</p>
<img src={user.imageUrl} alt={user.firstName ?? ''} />
</div>
);
}
// API Route — check auth
// app/api/protected/route.ts
import { auth } from '@clerk/nextjs/server';
export async function GET() {
const { userId } = await auth();
if (!userId) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Fetch data for this user
const data = await db.query.posts.findMany({
where: eq(posts.authorId, userId),
});
return Response.json({ data });
}Fix 3: Custom Sign-In and Sign-Up Pages
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';
export default function SignInPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<SignIn
appearance={{
elements: {
rootBox: 'mx-auto',
card: 'shadow-lg',
},
}}
routing="path"
path="/sign-in"
signUpUrl="/sign-up"
/>
</div>
);
}
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs';
export default function SignUpPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<SignUp
routing="path"
path="/sign-up"
signInUrl="/sign-in"
/>
</div>
);
}Fix 4: Webhook Handling (Sync Users to Database)
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
import { headers } from 'next/headers';
import type { WebhookEvent } from '@clerk/nextjs/server';
export async function POST(req: Request) {
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET!;
const headerPayload = await headers();
const svix_id = headerPayload.get('svix-id');
const svix_timestamp = headerPayload.get('svix-timestamp');
const svix_signature = headerPayload.get('svix-signature');
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response('Missing svix headers', { status: 400 });
}
const body = await req.text();
const wh = new Webhook(WEBHOOK_SECRET);
let event: WebhookEvent;
try {
event = wh.verify(body, {
'svix-id': svix_id,
'svix-timestamp': svix_timestamp,
'svix-signature': svix_signature,
}) as WebhookEvent;
} catch {
return new Response('Invalid signature', { status: 400 });
}
switch (event.type) {
case 'user.created': {
const { id, email_addresses, first_name, last_name, image_url } = event.data;
await db.insert(users).values({
clerkId: id,
email: email_addresses[0]?.email_address ?? '',
name: `${first_name ?? ''} ${last_name ?? ''}`.trim(),
avatar: image_url,
});
break;
}
case 'user.updated': {
const { id, email_addresses, first_name, last_name, image_url } = event.data;
await db.update(users).set({
email: email_addresses[0]?.email_address ?? '',
name: `${first_name ?? ''} ${last_name ?? ''}`.trim(),
avatar: image_url,
}).where(eq(users.clerkId, id));
break;
}
case 'user.deleted': {
if (event.data.id) {
await db.delete(users).where(eq(users.clerkId, event.data.id));
}
break;
}
}
return new Response('OK', { status: 200 });
}Fix 5: Custom Theme and Appearance
// app/layout.tsx — global appearance customization
import { ClerkProvider } from '@clerk/nextjs';
import { dark } from '@clerk/themes';
<ClerkProvider
appearance={{
baseTheme: dark, // Or import { shadesOfPurple } from '@clerk/themes'
variables: {
colorPrimary: '#3b82f6',
colorBackground: '#1a1a2e',
colorText: '#ffffff',
borderRadius: '8px',
},
elements: {
card: 'bg-gray-900 shadow-xl',
headerTitle: 'text-white',
socialButtonsBlockButton: 'bg-gray-800 border-gray-700 hover:bg-gray-700',
formFieldInput: 'bg-gray-800 border-gray-600',
footerActionLink: 'text-blue-400 hover:text-blue-300',
},
}}
>Fix 6: Organizations (Multi-Tenant)
// Enable organizations in Clerk Dashboard → Organizations → Enable
// Client — organization switcher
'use client';
import { OrganizationSwitcher, useOrganization } from '@clerk/nextjs';
function OrgHeader() {
const { organization, isLoaded } = useOrganization();
return (
<div>
<OrganizationSwitcher
hidePersonal={false}
afterCreateOrganizationUrl="/dashboard"
afterSelectOrganizationUrl="/dashboard"
/>
{organization && <p>Current org: {organization.name}</p>}
</div>
);
}
// Server — check organization membership
import { auth } from '@clerk/nextjs/server';
export async function GET() {
const { userId, orgId, orgRole } = await auth();
if (!orgId) {
return Response.json({ error: 'No organization selected' }, { status: 400 });
}
// orgRole: 'org:admin' | 'org:member' | custom roles
if (orgRole !== 'org:admin') {
return Response.json({ error: 'Admin access required' }, { status: 403 });
}
// Fetch org-specific data
const data = await db.query.projects.findMany({
where: eq(projects.organizationId, orgId),
});
return Response.json({ data });
}Still Not Working?
Components show infinite loading spinner — NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is missing or wrong. Check .env.local — the variable must start with pk_test_ (development) or pk_live_ (production). Also verify ClerkProvider wraps your entire app in layout.tsx.
Middleware causes redirect loop — the sign-in page is being protected by middleware. Add /sign-in(.*) and /sign-up(.*) to your public routes matcher. Also add any webhook endpoints: /api/webhooks(.*).
currentUser() returns null in Server Components — CLERK_SECRET_KEY is missing or invalid. This key (starting with sk_test_ or sk_live_) is needed for server-side Clerk API calls. Also ensure the middleware is running — it attaches auth data to the request.
Webhooks not firing — set up the webhook endpoint in Clerk Dashboard → Webhooks. The URL must be publicly reachable (not localhost). For local development, use ngrok to expose your local server. Install the svix package for signature verification.
For related auth issues, see Fix: Auth.js Not Working and Fix: Better Auth Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Better Auth Not Working — Login Failing, Session Null, or OAuth Callback Error
How to fix Better Auth issues — server and client setup, email/password and OAuth providers, session management, middleware protection, database adapters, and plugin configuration.
Fix: next-safe-action Not Working — Action Not Executing, Validation Errors Missing, or Type Errors
How to fix next-safe-action issues — action client setup, Zod schema validation, useAction and useOptimisticAction hooks, middleware, error handling, and authorization patterns.
Fix: nuqs Not Working — URL State Not Syncing, Type Errors, or Server Component Issues
How to fix nuqs URL search params state management — useQueryState and useQueryStates setup, parsers, server-side access, shallow routing, history mode, and Next.js App Router integration.
Fix: Vercel AI SDK Not Working — Streaming Not Rendering, useChat Stuck Loading, or Provider Errors
How to fix Vercel AI SDK issues — useChat and useCompletion hooks, streaming responses with streamText, provider configuration for OpenAI and Anthropic, tool calling, and Next.js integration.