Skip to main content

Set Up Clerk Authentication in Next.js 2026

·APIScout Team
Share:

Set Up Clerk Authentication in Next.js 2026

Clerk gives you a complete auth system with pre-built UI components. No building login forms, no managing sessions, no handling password resets. This guide gets you from zero to authenticated in under 5 minutes — and then covers the production details that matter.

TL;DR

  • Clerk's hosted UI components (SignIn, SignUp, UserButton) work out of the box with zero styling required — wrap your layout with ClerkProvider and add middleware.ts to protect routes
  • Always sync Clerk users to your database via webhooks — your app database needs to know about users for foreign key relationships, and polling is not a substitute
  • Organizations enable B2B multi-tenancy: orgId and orgRole are available in every server-side auth() call
  • Custom JWT claims let you add subscription tier, user role, and other data to Clerk tokens so downstream API handlers don't need extra database lookups
  • Production requires separate Clerk instances for dev and prod, plus production OAuth app credentials for each social provider

What You'll Build

  • Sign-in and sign-up flows (email, Google, GitHub)
  • Protected routes with middleware
  • User profile management
  • Organization/team support
  • Role-based access control

Prerequisites: Next.js 14+, Clerk account (free: 10,000 MAU).

1. Setup (2 minutes)

Install

npm install @clerk/nextjs

Environment Variables

Create a Clerk application at dashboard.clerk.com, then copy your keys:

# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...

Add Provider

// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}

Add Middleware

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isPublicRoute = createRouteMatcher([
  '/',
  '/pricing(.*)',
  '/blog(.*)',
  '/api/webhooks(.*)',
]);

export default clerkMiddleware(async (auth, request) => {
  if (!isPublicRoute(request)) {
    await auth.protect();
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

That's it. Authentication is now active. Users hitting any non-public route will be redirected to sign in.

2. Auth Components (1 minute)

Sign In / Sign Up Buttons

// components/Header.tsx
import {
  SignInButton,
  SignUpButton,
  SignedIn,
  SignedOut,
  UserButton,
} from '@clerk/nextjs';

export function Header() {
  return (
    <header>
      <nav>
        <SignedOut>
          <SignInButton mode="modal" />
          <SignUpButton mode="modal" />
        </SignedOut>
        <SignedIn>
          <UserButton afterSignOutUrl="/" />
        </SignedIn>
      </nav>
    </header>
  );
}

The UserButton renders a dropdown with profile, settings, and sign out — no code needed.

Custom Sign-In Page (Optional)

// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';

export default function SignInPage() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <SignIn
        appearance={{
          elements: {
            rootBox: 'mx-auto',
            card: 'shadow-xl',
          },
        }}
      />
    </div>
  );
}

Add to env:

NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up

3. Access User Data

Client-Side

'use client';
import { useUser } from '@clerk/nextjs';

export function Dashboard() {
  const { user, isLoaded } = useUser();

  if (!isLoaded) return <div>Loading...</div>;

  return (
    <div>
      <h1>Welcome, {user?.firstName}!</h1>
      <p>Email: {user?.primaryEmailAddress?.emailAddress}</p>
      <img src={user?.imageUrl} alt="Profile" width={64} />
    </div>
  );
}

Server-Side

// app/dashboard/page.tsx
import { currentUser } from '@clerk/nextjs/server';

export default async function DashboardPage() {
  const user = await currentUser();

  if (!user) return <div>Not signed in</div>;

  return (
    <div>
      <h1>Dashboard</h1>
      <p>User ID: {user.id}</p>
      <p>Email: {user.emailAddresses[0]?.emailAddress}</p>
    </div>
  );
}

In API Routes

// app/api/user/route.ts
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export async function GET() {
  const { userId } = await auth();

  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Fetch user-specific data
  const data = await getUserData(userId);
  return NextResponse.json(data);
}

4. Social Login

Enable Providers

In Clerk Dashboard → User & Authentication → Social Connections:

  • Toggle on: Google, GitHub, Apple, etc.
  • Add OAuth credentials for each provider

They appear automatically in your sign-in/sign-up components. No code changes needed.

5. Organizations

Enable Organizations

In Clerk Dashboard → Organizations → Enable.

Organization Switcher

import { OrganizationSwitcher } from '@clerk/nextjs';

export function OrgSwitcher() {
  return (
    <OrganizationSwitcher
      afterCreateOrganizationUrl="/dashboard"
      afterSelectOrganizationUrl="/dashboard"
    />
  );
}

Check Organization in API

import { auth } from '@clerk/nextjs/server';

export async function GET() {
  const { userId, orgId, orgRole } = await auth();

  if (!orgId) {
    return NextResponse.json({ error: 'No organization selected' }, { status: 400 });
  }

  // orgRole: 'org:admin' | 'org:member'
  if (orgRole !== 'org:admin') {
    return NextResponse.json({ error: 'Admin only' }, { status: 403 });
  }

  return NextResponse.json({ orgId, role: orgRole });
}

6. Webhooks

Sync Clerk events with your database:

Set Up Webhook

In Clerk Dashboard → Webhooks → Add Endpoint:

  • URL: https://your-app.com/api/webhooks/clerk
  • Events: user.created, user.updated, user.deleted

Handle Events

// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
import { headers } from 'next/headers';
import { WebhookEvent } from '@clerk/nextjs/server';

export async function POST(req: Request) {
  const body = await req.text();
  const headerPayload = headers();

  const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);

  let evt: WebhookEvent;
  try {
    evt = wh.verify(body, {
      'svix-id': headerPayload.get('svix-id')!,
      'svix-timestamp': headerPayload.get('svix-timestamp')!,
      'svix-signature': headerPayload.get('svix-signature')!,
    }) as WebhookEvent;
  } catch {
    return new Response('Invalid signature', { status: 400 });
  }

  switch (evt.type) {
    case 'user.created':
      await createUserInDB({
        clerkId: evt.data.id,
        email: evt.data.email_addresses[0]?.email_address,
        name: `${evt.data.first_name} ${evt.data.last_name}`,
      });
      break;

    case 'user.updated':
      await updateUserInDB(evt.data.id, evt.data);
      break;

    case 'user.deleted':
      await deleteUserFromDB(evt.data.id!);
      break;
  }

  return new Response('OK');
}

7. Theming

// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';
import { dark } from '@clerk/themes';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider
      appearance={{
        baseTheme: dark,
        variables: {
          colorPrimary: '#2563eb',
          borderRadius: '0.5rem',
        },
        elements: {
          card: 'shadow-lg',
          formButtonPrimary: 'bg-blue-600 hover:bg-blue-700',
        },
      }}
    >
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}

Clerk vs Auth0 vs NextAuth vs Supabase Auth

Choosing an authentication provider is one of the highest-leverage early decisions in a Next.js project. The wrong choice is expensive to undo.

Clerk is the fastest path to a production-quality auth experience. The pre-built UI components handle every edge case — email verification, password reset, MFA setup, OAuth account linking — without customization. The developer experience is excellent: adding auth to a new project takes minutes, and the middleware pattern for route protection is clean and idiomatic for Next.js App Router. Clerk is the right choice for teams that want great auth without building auth UI, and for B2B SaaS that needs Organizations out of the box. The pricing (free up to 10,000 MAU, then $0.02/MAU) is competitive for most applications. The limitation: you're locked into Clerk's hosted infrastructure and UI paradigms.

Auth0 is the enterprise option. It has been around longer, has more enterprise integrations (Active Directory, SAML, advanced MFA), and is the choice for organizations with strict compliance requirements or existing Auth0 relationships. Auth0's developer experience is significantly more complex than Clerk's for simple use cases, but its enterprise feature set is deeper. The pricing is more expensive for most indie/startup use cases. Choose Auth0 when your enterprise customers' IT departments need to configure SSO — Auth0's enterprise connection support is more mature.

NextAuth.js (Auth.js v5) is the right choice when you want full control and no vendor dependency. It's an open-source library that runs entirely on your infrastructure. Configuration requires more code than Clerk, and you're responsible for implementing the session/user storage yourself. The upside: zero per-user cost, complete data ownership, and flexibility to implement any auth flow. The downside: you're building and maintaining auth UI, handling edge cases, and managing session storage. NextAuth is excellent for developers who want to understand every layer of their auth stack and for applications with unusual auth requirements that hosted solutions don't support.

Supabase Auth is the natural choice when you're already using Supabase as your database. Auth is deeply integrated with Supabase's Row Level Security (RLS) — your database policies can reference auth.uid() to restrict data access to the authenticated user. This tight integration eliminates the user-syncing webhook pattern (because user data lives in the same Supabase database as your application data). The limitation: Supabase Auth is primarily designed for the Supabase ecosystem, and its UI components are less polished than Clerk's.

For a standard Next.js SaaS in 2026, the recommendation is Clerk unless you have a specific reason to choose differently. It handles the most complexity with the least code.

Organizations and Teams

Clerk Organizations is the feature that makes Clerk particularly compelling for B2B SaaS. An organization represents a company, team, or workspace in your product — a customer account that can have multiple members.

When Organizations is enabled, every Clerk user can belong to one or more organizations. The OrganizationSwitcher component handles creating new organizations and switching between them in the UI. In your API routes, orgId and orgRole are available from auth() without any additional setup.

Organization-scoped resources are the key pattern: when a user creates a project, record, or resource, tag it with the user's current orgId. When displaying resources, filter by orgId. This single pattern implements multi-tenancy.

// Creating an org-scoped resource
export async function POST(req: Request) {
  const { userId, orgId } = await auth();
  if (!userId || !orgId) return unauthorized();

  const project = await db.project.create({
    data: {
      name: 'New Project',
      organizationId: orgId,  // Scoped to the organization
      createdBy: userId,
    }
  });

  return NextResponse.json(project);
}

// Listing org-scoped resources
export async function GET(req: Request) {
  const { orgId } = await auth();
  if (!orgId) return unauthorized();

  const projects = await db.project.findMany({
    where: { organizationId: orgId }  // Only this org's projects
  });

  return NextResponse.json(projects);
}

Role checking within organizations uses the orgRole value from auth(). Clerk supports custom roles (configurable per organization beyond the default admin/member) on paid plans, which allows expressing more complex permission models.

Inviting members to an organization is handled by Clerk's hosted UI — the <OrganizationProfile> component includes an invitation flow. You can also trigger invitations programmatically via Clerk's Backend API.

Clerk Webhooks

The most important webhook pattern in Clerk is user synchronization. Your application database needs user records for foreign key relationships, custom fields (like subscription tier, feature flags, or preferences), and queries that would be impractical against Clerk's API. The webhook-based sync pattern creates a Clerk user record in your database whenever a user is created in Clerk.

This is different from just using Clerk's userId as a foreign key. You might have a users table with clerkId as the primary identifier and additional columns: plan, stripeCustomerId, createdAt, updatedAt. This table is populated by the user.created webhook and updated by user.updated. When a user is deleted in Clerk (user.deleted), you soft-delete or hard-delete the corresponding record depending on your data retention requirements.

Beyond user lifecycle events, organization.created and organization.membership.created are important for B2B SaaS — they let you set up default resources, send welcome emails, or trigger onboarding flows when new teams are created.

Clerk uses Svix under the hood for webhook delivery, which means all Clerk webhooks are signed with the Svix signature scheme and include delivery retry logic. The webhook secret in your environment (CLERK_WEBHOOK_SECRET) is used to verify signatures as shown in section 6 above. For a detailed treatment of webhook security patterns, see our guide on building webhooks that don't break.

Custom Claims and JWT Templates

By default, Clerk JWTs contain the user's Clerk ID and organization information. Custom claims let you add application-specific data — like the user's subscription tier or role — so that API handlers have all the context they need from the token alone, without a database lookup.

Configure custom claims in Clerk Dashboard → JWT Templates → Clerk (the default template):

{
  "metadata": "{{user.public_metadata}}"
}

Public metadata is set server-side via Clerk's Backend API:

// Set during subscription webhook handling
await clerkClient.users.updateUserMetadata(clerkUserId, {
  publicMetadata: {
    plan: 'pro',
    planExpiresAt: '2027-03-08T00:00:00Z',
  }
});

Reading custom claims in an API route:

import { auth } from '@clerk/nextjs/server';

export async function GET() {
  const { sessionClaims } = await auth();

  const plan = sessionClaims?.metadata?.plan as string;

  if (plan !== 'pro') {
    return NextResponse.json({ error: 'Pro plan required' }, { status: 403 });
  }

  return NextResponse.json({ data: 'Pro-only data' });
}

This pattern eliminates a database lookup on every API request. The claim is embedded in the JWT, which is verified locally by Clerk's middleware. The trade-off: claims are only as fresh as the session — if you downgrade a user's plan, their existing JWT still contains plan: pro until it expires (typically 1 hour). For plan enforcement, this is usually acceptable. For immediate enforcement (user is suspended, access revoked), you need server-side session validation with auth({ verify: true }).

Production Hardening

Moving from development to production requires several configuration changes that are easy to miss.

Create a separate Clerk application for production. In the Clerk dashboard, "Development" instances use Clerk's shared OAuth app credentials and have relaxed security settings for developer convenience. Production instances require your own OAuth app credentials for each social provider (Google Cloud Console for Google, GitHub Developer Settings for GitHub, etc.). This is not optional — production deployments with dev-instance OAuth credentials will break when Clerk's shared credentials exceed their quotas.

Configure allowed origins in your Clerk dashboard's Advanced settings. Clerk validates that requests originate from your configured domains. In production, add https://yourdomain.com and any other origins your application uses. Missing this causes auth failures in production that don't appear in development.

Session settings that matter for production: session token expiry (default is 1 hour, appropriate for most applications; reduce to 15 minutes for high-security applications), session inactivity timeout (automatically sign out users after N minutes of inactivity), and multi-session support (whether users can be signed in to the same app in multiple tabs/windows simultaneously).

Environment variable separation: NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY for your production instance are different from your development instance keys. Use your CI/CD platform's environment variable management to ensure production deployments use production Clerk keys. Never use development keys in production.

Pricing

PlanMAUPrice
Free10,000$0
Pro10,000+$25/month + $0.02/MAU over 10K
EnterpriseCustomCustom

Free tier includes: unlimited social logins, organizations, custom domains.

Common Mistakes

MistakeImpactFix
Forgetting middlewareNo routes are protectedAlways add middleware.ts
Not syncing with databaseCan't query users in your DBSet up webhooks for user events
Client-side only auth checksSecurity bypassUse auth() server-side for sensitive routes
Not handling org contextMulti-tenant bugsCheck orgId in org-scoped routes
Exposing secret keyAccount compromiseOnly NEXT_PUBLIC_ key goes to client
Dev keys in productionOAuth failuresCreate separate production Clerk app

Choosing auth? Explore authentication APIs on APIScout. Also see our guides on API authentication patterns and API security checklist for building secure applications.

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.