Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds
Volver a colecciones
Guide16 min read

Clerk

Clerk is a complete authentication and user management platform for React, Next.js and other frameworks. It offers ready-to-use UI components, SSO, MFA, organizations and webhooks.

Clerk - Complete authentication for modern applications

What is Clerk?

Clerk is a next-generation authentication platform that solves all problems related to user authentication in web applications. Unlike traditional solutions like Auth0 or Firebase Auth, Clerk offers:

  • Ready-to-use UI components - professional sign-in and sign-up forms
  • User management - a full administrative dashboard
  • Organizations and teams - multi-tenant with roles and permissions
  • Webhooks - automation on user events
  • Edge-ready - works in serverless and edge environments

Clerk is particularly popular in the Next.js ecosystem, where integration takes literally just a few minutes.

Why Clerk?

The problem with authentication

Implementing authentication from scratch is a nightmare:

  • Password hashing and security
  • Sessions, tokens, refreshing
  • Social login (Google, GitHub, Apple...)
  • Password reset, email verification
  • MFA/2FA
  • Session management across multiple devices
  • GDPR, compliance, audits

Clerk solves everything

Code
TypeScript
// This is all you need for full authentication!
import { SignIn, SignUp, UserButton } from '@clerk/nextjs'

export default function Auth() {
  return (
    <>
      <SignIn />      {/* Sign-in form */}
      <SignUp />      {/* Sign-up form */}
      <UserButton />  {/* Avatar with user menu */}
    </>
  )
}

Installation and configuration

Next.js (App Router)

Code
Bash
npm install @clerk/nextjs

API keys

Create an account at clerk.com and get your keys:

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

# Optional - redirect paths
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding

Middleware (route protection)

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

// Define public routes
const isPublicRoute = createRouteMatcher([
  '/',
  '/sign-in(.*)',
  '/sign-up(.*)',
  '/api/webhooks(.*)',
])

export default clerkMiddleware((auth, request) => {
  // Protected routes require authentication
  if (!isPublicRoute(request)) {
    auth().protect()
  }
})

export const config = {
  matcher: [
    // Skip static files and _next
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
  ],
}

ClerkProvider

TSapp/layout.tsx
TypeScript
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'
import { enUS } from '@clerk/localizations'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <ClerkProvider
      localization={enUS}  // English localization
      appearance={{
        elements: {
          // Appearance customization
          formButtonPrimary: 'bg-blue-600 hover:bg-blue-700',
          card: 'shadow-lg',
        }
      }}
    >
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  )
}

UI components

SignIn and SignUp

TSapp/sign-in/[[...sign-in]]/page.tsx
TypeScript
// 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: 'bg-white shadow-xl rounded-xl',
          }
        }}
        routing="path"
        path="/sign-in"
        signUpUrl="/sign-up"
      />
    </div>
  )
}
TSapp/sign-up/[[...sign-up]]/page.tsx
TypeScript
// 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>
  )
}

UserButton and UserProfile

Code
TypeScript
import { UserButton, UserProfile } from '@clerk/nextjs'

export function Header() {
  return (
    <header className="flex justify-between p-4">
      <Logo />
      <UserButton
        afterSignOutUrl="/"
        appearance={{
          elements: {
            avatarBox: 'w-10 h-10',
          }
        }}
      >
        {/* Custom menu items */}
        <UserButton.MenuItems>
          <UserButton.Link
            label="Settings"
            labelIcon={<SettingsIcon />}
            href="/settings"
          />
          <UserButton.Action label="Help" onClick={() => openHelp()} />
        </UserButton.MenuItems>
      </UserButton>
    </header>
  )
}

// Full profile page
export function ProfilePage() {
  return (
    <UserProfile
      appearance={{
        elements: {
          card: 'shadow-none',
        }
      }}
    />
  )
}

SignedIn and SignedOut

Code
TypeScript
import { SignedIn, SignedOut, SignInButton } from '@clerk/nextjs'

export function AuthStatus() {
  return (
    <>
      <SignedIn>
        {/* Visible only to signed-in users */}
        <p>You are signed in!</p>
        <UserButton />
      </SignedIn>

      <SignedOut>
        {/* Visible only to signed-out users */}
        <SignInButton mode="modal">
          <button className="px-4 py-2 bg-blue-600 text-white rounded">
            Sign in
          </button>
        </SignInButton>
      </SignedOut>
    </>
  )
}

Server-side authentication

Server Components

TSapp/dashboard/page.tsx
TypeScript
// 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>Welcome, {user.firstName}!</h1>
      <p>Email: {user.emailAddresses[0].emailAddress}</p>
      <p>ID: {user.id}</p>

      {/* User metadata */}
      <pre>{JSON.stringify(user.publicMetadata, null, 2)}</pre>
    </div>
  )
}

auth() helper

Code
TypeScript
import { auth } from '@clerk/nextjs/server'

export default async function ProtectedPage() {
  const { userId, sessionClaims, orgId, orgRole } = auth()

  if (!userId) {
    return <div>Unauthorized</div>
  }

  // Check role in organization
  if (orgRole !== 'admin') {
    return <div>No administrator permissions</div>
  }

  return <AdminDashboard />
}

Server Actions

Code
TypeScript
'use server'

import { auth, currentUser } from '@clerk/nextjs/server'
import { revalidatePath } from 'next/cache'

export async function updateProfile(formData: FormData) {
  const { userId } = auth()

  if (!userId) {
    throw new Error('Unauthorized')
  }

  const name = formData.get('name') as string

  await db.users.update({
    where: { clerkId: userId },
    data: { name }
  })

  revalidatePath('/profile')
}

export async function createPost(formData: FormData) {
  const user = await currentUser()

  if (!user) {
    throw new Error('You must be signed in')
  }

  const title = formData.get('title') as string
  const content = formData.get('content') as string

  await db.posts.create({
    data: {
      title,
      content,
      authorId: user.id,
      authorName: user.firstName,
    }
  })

  revalidatePath('/posts')
}

API Routes

TSapp/api/users/route.ts
TypeScript
// app/api/users/route.ts
import { auth } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'

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

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

  const user = await db.users.findUnique({
    where: { clerkId: userId }
  })

  return NextResponse.json(user)
}

export async function POST(request: Request) {
  const { userId } = auth()

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

  const body = await request.json()

  // Only admins can create users
  const currentUserData = await db.users.findUnique({
    where: { clerkId: userId }
  })

  if (currentUserData?.role !== 'admin') {
    return NextResponse.json(
      { error: 'Forbidden' },
      { status: 403 }
    )
  }

  const newUser = await db.users.create({ data: body })

  return NextResponse.json(newUser, { status: 201 })
}

Client-side hooks

useUser

Code
TypeScript
'use client'

import { useUser } from '@clerk/nextjs'

export function ProfileCard() {
  const { isLoaded, isSignedIn, user } = useUser()

  if (!isLoaded) {
    return <Skeleton />
  }

  if (!isSignedIn) {
    return <SignInPrompt />
  }

  return (
    <div className="p-6 bg-white rounded-lg shadow">
      <img
        src={user.imageUrl}
        alt={user.fullName || 'Avatar'}
        className="w-20 h-20 rounded-full"
      />
      <h2 className="mt-4 text-xl font-bold">{user.fullName}</h2>
      <p className="text-gray-500">
        {user.primaryEmailAddress?.emailAddress}
      </p>

      {/* Metadata */}
      <div className="mt-4">
        <span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-sm">
          {user.publicMetadata.plan || 'Free'}
        </span>
      </div>
    </div>
  )
}

useAuth

Code
TypeScript
'use client'

import { useAuth } from '@clerk/nextjs'

export function ProtectedAction() {
  const { isLoaded, userId, sessionId, getToken } = useAuth()

  async function callProtectedAPI() {
    // Get JWT token
    const token = await getToken()

    const response = await fetch('/api/protected', {
      headers: {
        Authorization: `Bearer ${token}`
      }
    })

    return response.json()
  }

  async function callExternalAPI() {
    // Token for an external API (e.g. Supabase)
    const token = await getToken({ template: 'supabase' })

    // Use the token with the Supabase client
  }

  if (!isLoaded) return <Loading />
  if (!userId) return <SignInRequired />

  return (
    <button onClick={callProtectedAPI}>
      Call protected API
    </button>
  )
}

useSession

Code
TypeScript
'use client'

import { useSession } from '@clerk/nextjs'

export function SessionInfo() {
  const { isLoaded, session } = useSession()

  if (!isLoaded || !session) return null

  return (
    <div>
      <p>Session ID: {session.id}</p>
      <p>Created: {session.createdAt.toLocaleDateString()}</p>
      <p>Last activity: {session.lastActiveAt.toLocaleDateString()}</p>

      <button onClick={() => session.end()}>
        Sign out from this device
      </button>
    </div>
  )
}

Organizations (multi-tenant)

Organization configuration

TSapp/layout.tsx
TypeScript
// app/layout.tsx - enable organizations
<ClerkProvider
  organizationSyncOptions={{
    enabled: true,
    syncOnRedirect: true,
  }}
>
  {children}
</ClerkProvider>

Organization components

Code
TypeScript
import {
  OrganizationSwitcher,
  OrganizationProfile,
  CreateOrganization,
  OrganizationList,
} from '@clerk/nextjs'

export function OrgHeader() {
  return (
    <header className="flex items-center gap-4">
      {/* Organization switcher */}
      <OrganizationSwitcher
        hidePersonal  // Hide personal account
        afterSelectOrganizationUrl="/org/:slug"
        afterCreateOrganizationUrl="/org/:slug"
        appearance={{
          elements: {
            organizationSwitcherTrigger: 'border rounded px-3 py-2',
          }
        }}
      />
    </header>
  )
}

// Organization management page
export function OrgSettingsPage() {
  return (
    <OrganizationProfile
      appearance={{
        elements: {
          card: 'shadow-lg',
        }
      }}
    />
  )
}

// User's organization list
export function MyOrganizations() {
  return (
    <OrganizationList
      afterSelectOrganizationUrl="/org/:slug"
      afterCreateOrganizationUrl="/org/:slug/settings"
    />
  )
}

useOrganization hook

Code
TypeScript
'use client'

import { useOrganization, useOrganizationList } from '@clerk/nextjs'

export function TeamDashboard() {
  const { organization, membership, isLoaded } = useOrganization()

  if (!isLoaded) return <Loading />
  if (!organization) return <NoOrgSelected />

  const isAdmin = membership?.role === 'admin'

  return (
    <div>
      <h1>{organization.name}</h1>
      <img src={organization.imageUrl} alt={organization.name} />

      {/* Only admin sees settings */}
      {isAdmin && (
        <button onClick={() => openSettings()}>
          Team settings
        </button>
      )}

      {/* Members list */}
      <MembersList orgId={organization.id} />
    </div>
  )
}

export function OrgSelector() {
  const { organizationList, isLoaded, setActive } = useOrganizationList()

  if (!isLoaded) return <Loading />

  return (
    <ul>
      {organizationList?.map(({ organization }) => (
        <li key={organization.id}>
          <button onClick={() => setActive({ organization: organization.id })}>
            {organization.name}
          </button>
        </li>
      ))}
    </ul>
  )
}

Roles and permissions

TSmiddleware.ts
TypeScript
// middleware.ts - organization route protection
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isOrgRoute = createRouteMatcher(['/org/:slug(.*)'])
const isAdminRoute = createRouteMatcher(['/org/:slug/admin(.*)'])

export default clerkMiddleware((auth, request) => {
  // Organization routes require membership
  if (isOrgRoute(request)) {
    auth().protect({
      organizationId: true,  // Requires active organization
    })
  }

  // Admin routes require the admin role
  if (isAdminRoute(request)) {
    auth().protect({
      role: 'org:admin',
    })
  }
})
Code
TypeScript
// Checking roles in a component
import { Protect } from '@clerk/nextjs'

export function AdminPanel() {
  return (
    <Protect
      role="org:admin"
      fallback={<p>Only administrators have access</p>}
    >
      <AdminDashboard />
    </Protect>
  )
}

// Or with a hook
export function RoleBasedUI() {
  const { has } = useAuth()

  const canManageMembers = has({ role: 'org:admin' })
  const canViewReports = has({ permission: 'org:reports:read' })

  return (
    <div>
      {canManageMembers && <MembersManager />}
      {canViewReports && <ReportsViewer />}
    </div>
  )
}

Webhooks

Webhook configuration

TSapp/api/webhooks/clerk/route.ts
TypeScript
// 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 WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET

  if (!WEBHOOK_SECRET) {
    throw new Error('CLERK_WEBHOOK_SECRET is required')
  }

  // Get headers
  const headerPayload = 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 })
  }

  // Get body
  const payload = await req.json()
  const body = JSON.stringify(payload)

  // Verify webhook
  const wh = new Webhook(WEBHOOK_SECRET)
  let evt: WebhookEvent

  try {
    evt = wh.verify(body, {
      'svix-id': svix_id,
      'svix-timestamp': svix_timestamp,
      'svix-signature': svix_signature,
    }) as WebhookEvent
  } catch (err) {
    console.error('Webhook verification failed:', err)
    return new Response('Invalid signature', { status: 400 })
  }

  // Handle events
  const eventType = evt.type

  switch (eventType) {
    case 'user.created':
      await handleUserCreated(evt.data)
      break

    case 'user.updated':
      await handleUserUpdated(evt.data)
      break

    case 'user.deleted':
      await handleUserDeleted(evt.data)
      break

    case 'organization.created':
      await handleOrgCreated(evt.data)
      break

    case 'organizationMembership.created':
      await handleMemberAdded(evt.data)
      break

    default:
      console.log(`Unhandled event type: ${eventType}`)
  }

  return new Response('OK', { status: 200 })
}

async function handleUserCreated(data: any) {
  const { id, email_addresses, first_name, last_name, image_url } = data

  await db.users.create({
    data: {
      clerkId: id,
      email: email_addresses[0]?.email_address,
      firstName: first_name,
      lastName: last_name,
      imageUrl: image_url,
    }
  })

  // Send welcome email
  await sendWelcomeEmail(email_addresses[0]?.email_address)
}

async function handleUserDeleted(data: any) {
  const { id } = data

  // Soft delete or anonymization
  await db.users.update({
    where: { clerkId: id },
    data: {
      deletedAt: new Date(),
      email: null,
      firstName: 'Deleted',
      lastName: 'User',
    }
  })
}

Database integration

User synchronization

TSlib/sync-user.ts
TypeScript
// lib/sync-user.ts
import { currentUser } from '@clerk/nextjs/server'
import { db } from '@/lib/db'

export async function syncUser() {
  const clerkUser = await currentUser()

  if (!clerkUser) return null

  // Find or create user in the database
  let user = await db.users.findUnique({
    where: { clerkId: clerkUser.id }
  })

  if (!user) {
    user = await db.users.create({
      data: {
        clerkId: clerkUser.id,
        email: clerkUser.emailAddresses[0]?.emailAddress,
        firstName: clerkUser.firstName,
        lastName: clerkUser.lastName,
        imageUrl: clerkUser.imageUrl,
      }
    })
  }

  return user
}

// Usage in a Server Component
export default async function DashboardPage() {
  const user = await syncUser()

  if (!user) redirect('/sign-in')

  const posts = await db.posts.findMany({
    where: { authorId: user.id }
  })

  return <PostsList posts={posts} />
}

Prisma Schema

prisma/schema.prisma
Prisma
// prisma/schema.prisma
model User {
  id        String   @id @default(cuid())
  clerkId   String   @unique
  email     String?  @unique
  firstName String?
  lastName  String?
  imageUrl  String?
  role      Role     @default(USER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  posts     Post[]
  comments  Comment[]
}

enum Role {
  USER
  ADMIN
  MODERATOR
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  String
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
}

Social login and SSO

Dashboard configuration

In the Clerk dashboard, enable providers:

  • Google OAuth
  • GitHub OAuth
  • Apple Sign In
  • Microsoft
  • Discord
  • Twitch
  • LinkedIn
  • Facebook

SAML SSO (Enterprise)

Code
TypeScript
// For enterprise customers with their own IdP
<SignIn
  appearance={{
    elements: {
      socialButtonsBlockButton: 'flex items-center gap-2',
    }
  }}
/>

Custom OAuth

TSapp/api/oauth/custom/route.ts
TypeScript
// app/api/oauth/custom/route.ts
import { auth } from '@clerk/nextjs/server'

export async function GET(request: Request) {
  const { userId } = auth()

  // Redirect to external OAuth
  const authUrl = new URL('https://external-service.com/oauth/authorize')
  authUrl.searchParams.set('client_id', process.env.EXTERNAL_CLIENT_ID!)
  authUrl.searchParams.set('redirect_uri', `${process.env.NEXT_PUBLIC_URL}/api/oauth/callback`)
  authUrl.searchParams.set('state', userId || '')

  return Response.redirect(authUrl.toString())
}

Appearance customization

Global customization

TSapp/layout.tsx
TypeScript
// app/layout.tsx
<ClerkProvider
  appearance={{
    // Base theme
    baseTheme: dark,

    // CSS variables
    variables: {
      colorPrimary: '#6366f1',
      colorBackground: '#1f2937',
      colorText: '#f9fafb',
      colorInputBackground: '#374151',
      borderRadius: '0.5rem',
      fontFamily: 'Inter, sans-serif',
    },

    // Elements
    elements: {
      // Main card
      card: 'bg-gray-800 border border-gray-700 shadow-xl',

      // Buttons
      formButtonPrimary:
        'bg-indigo-600 hover:bg-indigo-700 text-white font-medium',
      formButtonReset:
        'text-gray-400 hover:text-white',

      // Inputs
      formFieldInput:
        'bg-gray-700 border-gray-600 text-white placeholder-gray-400',
      formFieldLabel:
        'text-gray-300',

      // Social buttons
      socialButtonsBlockButton:
        'bg-gray-700 border-gray-600 hover:bg-gray-600',
      socialButtonsBlockButtonText:
        'text-white font-medium',

      // Divider
      dividerLine: 'bg-gray-600',
      dividerText: 'text-gray-400',

      // Footer
      footerActionLink:
        'text-indigo-400 hover:text-indigo-300',
    },

    // Layouts
    layout: {
      socialButtonsPlacement: 'top',
      socialButtonsVariant: 'blockButton',
      termsPageUrl: '/terms',
      privacyPageUrl: '/privacy',
    },
  }}
>

Per-component customization

Code
TypeScript
<SignIn
  appearance={{
    elements: {
      rootBox: 'mx-auto w-full max-w-md',
      card: 'rounded-2xl shadow-2xl',
      headerTitle: 'text-2xl font-bold text-center',
      headerSubtitle: 'text-gray-500 text-center',
      formButtonPrimary: 'w-full py-3 text-lg',
    }
  }}
/>

MFA/2FA

Enabling 2FA

In the Clerk dashboard, enable:

  • SMS OTP
  • Authenticator apps (TOTP)
  • Backup codes

Enforcing 2FA

TSapp/settings/security/page.tsx
TypeScript
// app/settings/security/page.tsx
import { UserProfile } from '@clerk/nextjs'

export default function SecuritySettings() {
  return (
    <UserProfile>
      <UserProfile.Page label="Security" url="security">
        {/* The 2FA section is built-in */}
      </UserProfile.Page>
    </UserProfile>
  )
}
Code
TypeScript
// Checking 2FA in middleware
export default clerkMiddleware((auth, request) => {
  const { sessionClaims } = auth()

  // Require 2FA for sensitive routes
  if (request.url.includes('/admin')) {
    if (!sessionClaims?.['2fa_enabled']) {
      return Response.redirect(new URL('/setup-2fa', request.url))
    }
  }
})

User metadata

Public vs Private Metadata

Code
TypeScript
// Public metadata - visible on the client side
// Use for: subscription plan, role, UI preferences

// Private metadata - server-side only
// Use for: Stripe customer ID, internal flags

// Setting via API
import { clerkClient } from '@clerk/nextjs/server'

async function upgradeToPro(userId: string, stripeCustomerId: string) {
  await clerkClient.users.updateUser(userId, {
    publicMetadata: {
      plan: 'pro',
      planExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
    },
    privateMetadata: {
      stripeCustomerId,
    },
  })
}

// Reading in a Server Component
const user = await currentUser()
const plan = user?.publicMetadata.plan as string || 'free'

// Reading in a Client Component
const { user } = useUser()
const plan = user?.publicMetadata.plan as string || 'free'

Unsafe Metadata (user-editable)

Code
TypeScript
// Users can edit unsafe metadata
'use client'

import { useUser } from '@clerk/nextjs'

export function PreferencesForm() {
  const { user } = useUser()

  async function updatePreferences(formData: FormData) {
    await user?.update({
      unsafeMetadata: {
        theme: formData.get('theme'),
        language: formData.get('language'),
        notifications: formData.get('notifications') === 'on',
      }
    })
  }

  return (
    <form action={updatePreferences}>
      <select name="theme">
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
      {/* ... */}
    </form>
  )
}

JWT Templates

Supabase integration

Code
TypeScript
// In the Clerk Dashboard, create a JWT template called "supabase"
// with claims:
// {
//   "sub": "{{user.id}}",
//   "email": "{{user.primary_email_address}}",
//   "role": "authenticated"
// }

// Usage
'use client'

import { useAuth } from '@clerk/nextjs'
import { createClient } from '@supabase/supabase-js'

export function useSupabase() {
  const { getToken } = useAuth()

  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      global: {
        fetch: async (url, options = {}) => {
          const clerkToken = await getToken({ template: 'supabase' })

          return fetch(url, {
            ...options,
            headers: {
              ...options.headers,
              Authorization: `Bearer ${clerkToken}`,
            },
          })
        },
      },
    }
  )

  return supabase
}

Clerk pricing (2025)

PlanPriceMAUFeatures
Free$010,000Basic auth, 5 social providers
Pro$25/mo+$0.02/MAUUnlimited socials, custom domains
EnterpriseCustomUnlimitedSAML SSO, SLA, support

What is included in Free:

  • 10,000 Monthly Active Users
  • Email/password auth
  • 5 social providers
  • Organizations (up to 5)
  • Webhooks
  • Pre-built UI components

Pro adds:

  • Unlimited social providers
  • Custom domains
  • SAML SSO
  • Allowlists/blocklists
  • Bot protection
  • Advanced customization

Clerk vs alternatives

FeatureClerkAuth0NextAuthFirebase
Setup5 min30 min1h15 min
UI ComponentsReady-madeNoneNoneBasic
OrganizationsYesYes (Pro)NoNo
Free tier10K MAU7K MAUUnlimited50K/mo
Edge supportYesNoYesNo
WebhooksYesYesNoYes
Self-hostNoNoYesNo

FAQ

How do I change the language to Polish?

Code
TypeScript
import { plPL } from '@clerk/localizations'

<ClerkProvider localization={plPL}>

How do I add custom fields during sign-up?

In the Clerk Dashboard, go to User & Authentication, then Email, Phone, Username, and add custom fields.

Does Clerk work with Pages Router?

Yes, Clerk supports both App Router and Pages Router.

How do I test locally?

Use test keys (pk_test_ and sk_test_). Clerk does not require tunnels for development.

How do I integrate with Stripe?

Use webhooks for synchronization and private metadata to store the Stripe customer ID.

Summary

Clerk is the best authentication solution for Next.js:

  • Fast integration - working auth in minutes
  • Ready-made UI - professional components
  • Organizations - multi-tenant out of the box
  • Type-safe - full TypeScript support
  • Edge-ready - works in serverless and edge
  • Generous free tier - 10K MAU for free

Use Clerk when you value your time and want to focus on building your product, not on authentication.