Skip to content

Fix: Clerk Not Working — Auth Not Loading, Middleware Blocking, or User Data Missing

FixDevs ·

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 in

Why This Happens

Clerk is a managed authentication service. Your app communicates with Clerk’s servers for all auth operations:

  • ClerkProvider must 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 setNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY are 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 keycurrentUser() and auth() in Server Components communicate with Clerk’s API using CLERK_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 spinnerNEXT_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 ComponentsCLERK_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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles