Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik15 min czytania

Clerk

Clerk to kompletna platforma autentykacji i zarządzania użytkownikami dla aplikacji React, Next.js i innych frameworków. Oferuje gotowe komponenty UI, SSO, MFA, organizacje i webhooks.

Clerk - Kompletna Autentykacja dla Nowoczesnych Aplikacji

Czym jest Clerk?

Clerk to platforma autentykacji nowej generacji, która rozwiązuje wszystkie problemy związane z uwierzytelnianiem użytkowników w aplikacjach webowych. W przeciwieństwie do tradycyjnych rozwiązań jak Auth0 czy Firebase Auth, Clerk oferuje:

  • Gotowe komponenty UI - profesjonalne formularze logowania i rejestracji
  • Zarządzanie użytkownikami - pełny dashboard administracyjny
  • Organizacje i zespoły - multi-tenant z rolami i uprawnieniami
  • Webhooks - automatyzacja na zdarzeniach użytkowników
  • Edge-ready - działa w środowiskach serverless i edge

Clerk jest szczególnie popularny w ekosystemie Next.js, gdzie integracja zajmuje dosłownie kilka minut.

Dlaczego Clerk?

Problem z autentykacją

Implementacja autentykacji od zera to koszmar:

  • Hashowanie haseł i bezpieczeństwo
  • Sesje, tokeny, odświeżanie
  • Social login (Google, GitHub, Apple...)
  • Reset hasła, weryfikacja email
  • MFA/2FA
  • Zarządzanie sesjami na wielu urządzeniach
  • GDPR, compliance, audyty

Clerk rozwiązuje wszystko

Code
TypeScript
// To wszystko co potrzebujesz do pełnej autentykacji!
import { SignIn, SignUp, UserButton } from '@clerk/nextjs'

export default function Auth() {
  return (
    <>
      <SignIn />      {/* Formularz logowania */}
      <SignUp />      {/* Formularz rejestracji */}
      <UserButton />  {/* Avatar z menu użytkownika */}
    </>
  )
}

Instalacja i Konfiguracja

Next.js (App Router)

Code
Bash
npm install @clerk/nextjs

Klucze API

Utwórz konto na clerk.com i pobierz klucze:

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

# Opcjonalne - ścieżki przekierowań
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 (ochrona tras)

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

// Definiuj publiczne trasy
const isPublicRoute = createRouteMatcher([
  '/',
  '/sign-in(.*)',
  '/sign-up(.*)',
  '/api/webhooks(.*)',
])

export default clerkMiddleware((auth, request) => {
  // Chronione trasy wymagają zalogowania
  if (!isPublicRoute(request)) {
    auth().protect()
  }
})

export const config = {
  matcher: [
    // Pomiń statyczne pliki i _next
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Zawsze uruchamiaj dla API
    '/(api|trpc)(.*)',
  ],
}

ClerkProvider

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <ClerkProvider
      localization={plPL}  // Polska lokalizacja
      appearance={{
        elements: {
          // Customizacja wyglądu
          formButtonPrimary: 'bg-blue-600 hover:bg-blue-700',
          card: 'shadow-lg',
        }
      }}
    >
      <html lang="pl">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  )
}

Komponenty UI

SignIn i 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 i 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',
          }
        }}
      >
        {/* Własne elementy w menu */}
        <UserButton.MenuItems>
          <UserButton.Link
            label="Ustawienia"
            labelIcon={<SettingsIcon />}
            href="/settings"
          />
          <UserButton.Action label="Pomoc" onClick={() => openHelp()} />
        </UserButton.MenuItems>
      </UserButton>
    </header>
  )
}

// Pełna strona profilu
export function ProfilePage() {
  return (
    <UserProfile
      appearance={{
        elements: {
          card: 'shadow-none',
        }
      }}
    />
  )
}

SignedIn i SignedOut

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

export function AuthStatus() {
  return (
    <>
      <SignedIn>
        {/* Widoczne tylko dla zalogowanych */}
        <p>Jesteś zalogowany!</p>
        <UserButton />
      </SignedIn>

      <SignedOut>
        {/* Widoczne tylko dla niezalogowanych */}
        <SignInButton mode="modal">
          <button className="px-4 py-2 bg-blue-600 text-white rounded">
            Zaloguj się
          </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>Witaj, {user.firstName}!</h1>
      <p>Email: {user.emailAddresses[0].emailAddress}</p>
      <p>ID: {user.id}</p>

      {/* Metadata użytkownika */}
      <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>
  }

  // Sprawdź rolę w organizacji
  if (orgRole !== 'admin') {
    return <div>Brak uprawnień administratora</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('Musisz być zalogowany')
  }

  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()

  // Tylko admini mogą tworzyć użytkowników
  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() {
    // Pobierz token JWT
    const token = await getToken()

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

    return response.json()
  }

  async function callExternalAPI() {
    // Token dla zewnętrznego API (np. Supabase)
    const token = await getToken({ template: 'supabase' })

    // Użyj tokena z Supabase client
  }

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

  return (
    <button onClick={callProtectedAPI}>
      Wywołaj chronione 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>Utworzona: {session.createdAt.toLocaleDateString()}</p>
      <p>Ostatnia aktywność: {session.lastActiveAt.toLocaleDateString()}</p>

      <button onClick={() => session.end()}>
        Wyloguj z tego urządzenia
      </button>
    </div>
  )
}

Organizacje (Multi-tenant)

Konfiguracja organizacji

TSapp/layout.tsx
TypeScript
// app/layout.tsx - włącz organizacje
<ClerkProvider
  organizationSyncOptions={{
    enabled: true,
    syncOnRedirect: true,
  }}
>
  {children}
</ClerkProvider>

Komponenty organizacji

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

export function OrgHeader() {
  return (
    <header className="flex items-center gap-4">
      {/* Przełącznik organizacji */}
      <OrganizationSwitcher
        hidePersonal  // Ukryj konto osobiste
        afterSelectOrganizationUrl="/org/:slug"
        afterCreateOrganizationUrl="/org/:slug"
        appearance={{
          elements: {
            organizationSwitcherTrigger: 'border rounded px-3 py-2',
          }
        }}
      />
    </header>
  )
}

// Strona zarządzania organizacją
export function OrgSettingsPage() {
  return (
    <OrganizationProfile
      appearance={{
        elements: {
          card: 'shadow-lg',
        }
      }}
    />
  )
}

// Lista organizacji użytkownika
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} />

      {/* Tylko admin widzi ustawienia */}
      {isAdmin && (
        <button onClick={() => openSettings()}>
          Ustawienia zespołu
        </button>
      )}

      {/* Lista członków */}
      <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>
  )
}

Role i uprawnienia

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

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

export default clerkMiddleware((auth, request) => {
  // Trasy organizacji wymagają członkostwa
  if (isOrgRoute(request)) {
    auth().protect({
      organizationId: true,  // Wymaga aktywnej organizacji
    })
  }

  // Trasy admin wymagają roli admin
  if (isAdminRoute(request)) {
    auth().protect({
      role: 'org:admin',
    })
  }
})
Code
TypeScript
// Sprawdzanie ról w komponencie
import { Protect } from '@clerk/nextjs'

export function AdminPanel() {
  return (
    <Protect
      role="org:admin"
      fallback={<p>Tylko administratorzy mają dostęp</p>}
    >
      <AdminDashboard />
    </Protect>
  )
}

// Lub z hookiem
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

Konfiguracja webhooków

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')
  }

  // Pobierz nagłówki
  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 })
  }

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

  // Zweryfikuj 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 })
  }

  // Obsłuż zdarzenia
  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,
    }
  })

  // Wyślij email powitalny
  await sendWelcomeEmail(email_addresses[0]?.email_address)
}

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

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

Integracja z Bazą Danych

Synchronizacja użytkowników

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

  // Znajdź lub utwórz użytkownika w bazie
  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
}

// Użycie w 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 i SSO

Konfiguracja w Dashboard

W dashboardzie Clerk włącz providery:

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

SAML SSO (Enterprise)

Code
TypeScript
// Dla klientów enterprise z własnym IdP
<SignIn
  appearance={{
    elements: {
      socialButtonsBlockButton: 'flex items-center gap-2',
    }
  }}
/>

Customowy 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()

  // Przekieruj do zewnętrznego 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())
}

Customizacja Wyglądu

Globalna customizacja

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

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

    // Elementy
    elements: {
      // Główna karta
      card: 'bg-gray-800 border border-gray-700 shadow-xl',

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

      // Inputy
      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',
    },

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

Per-komponent customizacja

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

Włączanie 2FA

W dashboardzie Clerk włącz:

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

Wymuszanie 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="Bezpieczeństwo" url="security">
        {/* Sekcja 2FA jest wbudowana */}
      </UserProfile.Page>
    </UserProfile>
  )
}
Code
TypeScript
// Sprawdzanie 2FA w middleware
export default clerkMiddleware((auth, request) => {
  const { sessionClaims } = auth()

  // Wymagaj 2FA dla wrażliwych tras
  if (request.url.includes('/admin')) {
    if (!sessionClaims?.['2fa_enabled']) {
      return Response.redirect(new URL('/setup-2fa', request.url))
    }
  }
})

Metadata Użytkownika

Public vs Private Metadata

Code
TypeScript
// Public metadata - widoczne po stronie klienta
// Użyj do: plan subskrypcji, rola, preferencje UI

// Private metadata - tylko server-side
// Użyj do: Stripe customer ID, internal flags

// Ustawianie z 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,
    },
  })
}

// Odczyt w Server Component
const user = await currentUser()
const plan = user?.publicMetadata.plan as string || 'free'

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

Unsafe Metadata (user-editable)

Code
TypeScript
// Użytkownik może edytować 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">Jasny</option>
        <option value="dark">Ciemny</option>
      </select>
      {/* ... */}
    </form>
  )
}

JWT Templates

Supabase Integration

Code
TypeScript
// W Clerk Dashboard utwórz JWT template "supabase"
// z claims:
// {
//   "sub": "{{user.id}}",
//   "email": "{{user.primary_email_address}}",
//   "role": "authenticated"
// }

// Użycie
'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
}

Cennik Clerk (2025)

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

Co wliczone w Free:

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

Pro dodaje:

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

Clerk vs Alternatywy

CechaClerkAuth0NextAuthFirebase
Setup5 min30 min1h15 min
UI Components✅ Gotowe✅ Basic
Organizations✅ Pro
Free tier10K MAU7K MAUUnlimited50K/mo
Edge support
Webhooks
Self-host

FAQ

Jak zmienić język na polski?

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

<ClerkProvider localization={plPL}>

Jak dodać własne pola przy rejestracji?

W Dashboard Clerk → User & Authentication → Email, Phone, Username → dodaj custom fields.

Czy Clerk działa z Pages Router?

Tak, Clerk wspiera zarówno App Router jak i Pages Router.

Jak testować lokalnie?

Użyj kluczy testowych (pk_test_ i sk_test_). Clerk nie wymaga tuneli dla developmentu.

Jak zintegrować z Stripe?

Użyj webhooków do synchronizacji i private metadata do przechowywania Stripe customer ID.

Podsumowanie

Clerk to najlepsze rozwiązanie autentykacji dla Next.js:

  • Szybka integracja - działające auth w minuty
  • Gotowe UI - profesjonalne komponenty
  • Organizacje - multi-tenant out of the box
  • Type-safe - pełne wsparcie TypeScript
  • Edge-ready - działa w serverless i edge
  • Hojny free tier - 10K MAU za darmo

Używaj Clerk gdy zależy Ci na czasie i chcesz skupić się na budowaniu produktu, nie na auth.