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
// 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)
npm install @clerk/nextjsKlucze API
Utwórz konto na clerk.com i pobierz klucze:
# .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=/onboardingMiddleware (ochrona tras)
// 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
// 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
// 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>
)
}// 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
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
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
// 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
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
'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
// 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
'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
'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
'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
// app/layout.tsx - włącz organizacje
<ClerkProvider
organizationSyncOptions={{
enabled: true,
syncOnRedirect: true,
}}
>
{children}
</ClerkProvider>Komponenty organizacji
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
'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
// 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',
})
}
})// 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
// 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
// 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
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
SAML SSO (Enterprise)
// Dla klientów enterprise z własnym IdP
<SignIn
appearance={{
elements: {
socialButtonsBlockButton: 'flex items-center gap-2',
}
}}
/>Customowy OAuth
// 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
// 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
<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
// 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>
)
}// 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
// 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)
// 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
// 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)
| Plan | Cena | MAU | Funkcje |
|---|---|---|---|
| Free | $0 | 10,000 | Basic auth, 5 social providers |
| Pro | $25/mo | +$0.02/MAU | Unlimited socials, custom domains |
| Enterprise | Custom | Unlimited | SAML 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
| Cecha | Clerk | Auth0 | NextAuth | Firebase |
|---|---|---|---|---|
| Setup | 5 min | 30 min | 1h | 15 min |
| UI Components | ✅ Gotowe | ❌ | ❌ | ✅ Basic |
| Organizations | ✅ | ✅ Pro | ❌ | ❌ |
| Free tier | 10K MAU | 7K MAU | Unlimited | 50K/mo |
| Edge support | ✅ | ❌ | ✅ | ❌ |
| Webhooks | ✅ | ✅ | ❌ | ✅ |
| Self-host | ❌ | ❌ | ✅ | ❌ |
FAQ
Jak zmienić język na polski?
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.