Authentication ist eine der komplexesten und fehleranfälligsten Komponenten moderner Webanwendungen. Clerk hat sich als führende Lösung für Next.js-Projekte etabliert — und Claude Code verändert grundlegend, wie schnell du eine vollständige Auth-Integration aufbaust. In diesem Artikel zeigen wir dir die gesamte Clerk-Integration: von der Middleware-Konfiguration bis zu Custom Claims, Organizations und Webhooks.
Was du in diesem Artikel lernst:
- Clerk Middleware mit clerkMiddleware und Route-Matching konfigurieren
- Client-seitige Auth-Hooks (useAuth, useUser) sicher einsetzen
- Server Components und Route Handler mit auth() absichern
- Multi-Tenancy mit Clerk Organizations implementieren
- Webhooks für User-Sync mit deiner Datenbank nutzen
- Custom Claims und RBAC für feingranulare Zugriffssteuerung
1. Clerk Setup & Next.js Middleware
Der Einstieg in Clerk beginnt mit der Installation und der Middleware-Konfiguration. Claude Code kann dir dabei helfen, die passende Middleware-Strategie für dein Projekt zu generieren — du beschreibst deine Route-Struktur, Claude Code liefert den fertigen Code.
Installation & Environment Variables
# Installation
npm install @clerk/nextjs
# .env.local — niemals in Git committen!
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
# Optional: Custom Sign-In / Sign-Up URLs
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
💡 Claude Code Prompt: "Erstelle eine Clerk-Middleware für Next.js 15 mit App Router. Öffentliche Routen: /, /blog/*, /pricing, /api/webhooks/*. Alle anderen Routen sollen authentifiziert sein."
clerkMiddleware mit createRouteMatcher
// middleware.ts — App Router (Next.js 14+)
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher([
'/',
'/blog(.*)',
'/pricing',
'/sign-in(.*)',
'/sign-up(.*)',
'/api/webhooks(.*)',
])
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect()
}
})
export const config = {
matcher: [
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
'/(api|trpc)(.*)',
],
}
Erweiterte Middleware: Rollen-basiertes Redirect
// middleware.ts — mit Role-Check
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
const isAdminRoute = createRouteMatcher(['/admin(.*)'])
const isPublicRoute = createRouteMatcher(['/', '/blog(.*)', '/api/webhooks(.*)'])
export default clerkMiddleware(async (auth, request) => {
const { userId, sessionClaims } = await auth()
// Admin-Route: nur für Admins
if (isAdminRoute(request)) {
if (!userId) {
return auth.redirectToSignIn()
}
const role = (sessionClaims?.metadata as { role?: string })?.role
if (role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
}
// Alle anderen geschützten Routen
if (!isPublicRoute(request)) {
await auth.protect()
}
})
💡 Claude Code Tipp: Beschreibe Claude Code deine komplette Route-Architektur in einem Satz. Claude Code erkennt automatisch, welche Routen öffentlich sein müssen und generiert den passenden createRouteMatcher-Aufruf.
⚠️ Wichtig: Die alte authMiddleware-API ist seit Clerk v5 deprecated. Nutze ausschließlich clerkMiddleware für neue Projekte.
ClerkProvider im Root Layout
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider
appearance={{
variables: { colorPrimary: '#6366f1' },
elements: {
formButtonPrimary: 'bg-indigo-600 hover:bg-indigo-700',
card: 'shadow-none border border-gray-200',
},
}}
>
<html lang="de">
<body className={inter.className}>{children}</body>
</html>
</ClerkProvider>
)
}
2. Client-Side Auth: useAuth, useUser & UI-Komponenten
Clerk bietet eine Reihe von React-Hooks und vorgefertigten UI-Komponenten, die du sofort in deine Client Components integrieren kannst. Claude Code kann aus einem Prompt vollständige Navigation-Komponenten mit Auth-State generieren.
useAuth
useUser
UserButton
SignInButton
useAuth Hook — Grundlegende Auth-Informationen
// components/AuthStatus.tsx
'use client'
import { useAuth } from '@clerk/nextjs'
export function AuthStatus() {
const {
isLoaded,
isSignedIn,
userId,
sessionId,
orgId,
orgRole,
orgSlug,
has, // Permission-Check
signOut,
getToken, // JWT für API-Calls
} = useAuth()
if (!isLoaded) return <div>Laden...</div>
if (!isSignedIn) return <div>Nicht angemeldet</div>
return (
<div>
<p>User ID: {userId}</p>
<p>Session: {sessionId}</p>
{orgId && <p>Organisation: {orgSlug} ({orgRole})</p>}
<button onClick={() => signOut()}>Abmelden</button>
</div>
)
}
useUser Hook — Profil-Daten
// components/UserProfile.tsx
'use client'
import { useUser } from '@clerk/nextjs'
export function UserProfile() {
const { isLoaded, isSignedIn, user } = useUser()
if (!isLoaded || !isSignedIn) return null
const primaryEmail = user.primaryEmailAddress?.emailAddress
const fullName = user.fullName ?? `${user.firstName} ${user.lastName}`
const avatarUrl = user.imageUrl
const createdAt = new Date(user.createdAt!).toLocaleDateString('de-DE')
return (
<div className="flex items-center gap-3 p-4 bg-gray-50 rounded-lg">
<img src={avatarUrl} alt={fullName} className="w-12 h-12 rounded-full" />
<div>
<p className="font-semibold">{fullName}</p>
<p className="text-sm text-gray-500">{primaryEmail}</p>
<p className="text-xs text-gray-400">Mitglied seit {createdAt}</p>
</div>
</div>
)
}
Navigation mit UserButton und bedingtem Rendering
// components/Navigation.tsx
'use client'
import {
SignInButton,
SignOutButton,
SignUpButton,
UserButton,
useAuth,
RedirectToSignIn,
} from '@clerk/nextjs'
import Link from 'next/link'
export function Navigation() {
const { isLoaded, isSignedIn } = useAuth()
return (
<nav className="flex items-center justify-between px-6 py-4 border-b">
<Link href="/" className="font-bold text-xl">
MyApp
</Link>
<div className="flex items-center gap-4">
{!isLoaded ? (
<div className="w-8 h-8 rounded-full bg-gray-200 animate-pulse" />
) : isSignedIn ? (
<>
<Link href="/dashboard">Dashboard</Link>
<UserButton
afterSignOutUrl="/"
appearance={{
elements: {
avatarBox: "w-9 h-9",
},
}}
/>
</>
) : (
<>
<SignInButton mode="modal">
<button className="text-gray-600 hover:text-gray-900">
Anmelden
</button>
</SignInButton>
<SignUpButton mode="modal">
<button className="bg-indigo-600 text-white px-4 py-2 rounded-lg">
Kostenlos starten
</button>
</SignUpButton>
</>
)}
</div>
</nav>
)
}
Geschützter Bereich mit RedirectToSignIn
// components/ProtectedContent.tsx
'use client'
import { useAuth, RedirectToSignIn } from '@clerk/nextjs'
export function ProtectedContent({ children }: { children: React.ReactNode }) {
const { isLoaded, isSignedIn } = useAuth()
if (!isLoaded) {
return <div className="flex justify-center p-8"><LoadingSpinner /></div>
}
if (!isSignedIn) {
return <RedirectToSignIn />
}
return <>{children}</>
}
💡 Claude Code Workflow: "Generiere eine vollständige Navigation mit Clerk Auth, die im nicht-eingeloggten Zustand Sign-In und Sign-Up Modals zeigt, und im eingeloggten Zustand einen UserButton mit Custom-Menüpunkten für Einstellungen und Billing."
3. Server-Side Auth: auth(), currentUser & Route Handler
Server Components und Route Handler sind der Kern moderner Next.js-Anwendungen. Clerk bietet mit auth() und currentUser() zwei mächtige Funktionen für die Server-seitige Auth. Claude Code versteht den Unterschied und wählt automatisch die richtige Funktion.
auth() in Server Components
// app/dashboard/page.tsx — Server Component
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const { userId, orgId, sessionClaims } = await auth()
if (!userId) {
redirect('/sign-in')
}
// Daten serverseitig laden — kein API-Round-Trip nötig
const userData = await db.user.findUnique({
where: { clerkId: userId },
include: { subscription: true, usage: true },
})
return (
<div>
<h1>Dashboard</h1>
<p>Willkommen zurück, {userData?.name}</p>
{orgId && <OrganizationDashboard orgId={orgId} />}
</div>
)
}
currentUser() — Vollständiges User-Objekt
// app/profile/page.tsx
import { currentUser } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function ProfilePage() {
const user = await currentUser()
if (!user) redirect('/sign-in')
// Direkter Zugriff auf alle Clerk-User-Felder
const {
id,
firstName,
lastName,
emailAddresses,
imageUrl,
publicMetadata,
privateMetadata,
createdAt,
} = user
const role = (publicMetadata as { role?: string }).role ?? 'user'
const primaryEmail = emailAddresses[0]?.emailAddress
return (
<ProfileForm
userId={id}
firstName={firstName ?? ''}
lastName={lastName ?? ''}
email={primaryEmail ?? ''}
role={role}
avatarUrl={imageUrl}
/>
)
}
Route Handler absichern
// app/api/projects/route.ts
import { auth } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
export async function GET() {
const { userId } = await auth()
if (!userId) {
return NextResponse.json({ error: 'Nicht authentifiziert' }, { status: 401 })
}
const projects = await db.project.findMany({
where: { ownerId: userId },
orderBy: { createdAt: 'desc' },
})
return NextResponse.json({ projects })
}
export async function POST(request: Request) {
const { userId, orgId } = await auth()
if (!userId) {
return NextResponse.json({ error: 'Nicht authentifiziert' }, { status: 401 })
}
const body = await request.json()
const { name, description } = body
if (!name?.trim()) {
return NextResponse.json({ error: 'Name ist erforderlich' }, { status: 400 })
}
const project = await db.project.create({
data: {
name: name.trim(),
description: description?.trim() ?? '',
ownerId: userId,
organizationId: orgId ?? null,
},
})
return NextResponse.json({ project }, { status: 201 })
}
auth() vs. currentUser() — Wann was verwenden?
| Funktion |
Gibt zurück |
Performance |
Empfehlung |
| auth() |
userId, sessionId, orgId, claims |
Sehr schnell (kein HTTP) |
Standard für Auth-Checks |
| currentUser() |
Vollständiges User-Objekt |
HTTP-Request zu Clerk |
Nur wenn User-Details nötig |
4. Organizations & Multi-Tenancy
Clerk Organizations ermöglichen Multi-Tenancy direkt aus der Box — ohne eigene Datenbankstruktur für Teams und Mitglieder. Claude Code kann komplette Multi-Tenant-Architekturen generieren, inklusive Organisation-Switching, Mitgliederverwaltung und rollenbasiertem Zugriff.
Organizations
Multi-Tenancy
Roles
useOrganization Hook
// components/OrgDashboard.tsx
'use client'
import { useOrganization, useOrganizationList } from '@clerk/nextjs'
export function OrgDashboard() {
const {
organization,
membership,
memberships,
isLoaded,
} = useOrganization({
memberships: { pageSize: 10 },
})
if (!isLoaded) return <LoadingSpinner />
if (!organization) return <NoOrgState />
const isAdmin = membership?.role === 'org:admin'
const memberCount = memberships?.count ?? 0
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<img
src={organization.imageUrl}
alt={organization.name}
className="w-16 h-16 rounded-xl"
/>
<div>
<h2 className="text-2xl font-bold">{organization.name}</h2>
<p className="text-gray-500">{memberCount} Mitglieder</p>
</div>
{isAdmin && <AdminBadge />}
</div>
<MemberList memberships={memberships?.data ?? []} isAdmin={isAdmin} />
</div>
)
}
OrganizationSwitcher — Multi-Org Navigation
// components/AppSidebar.tsx
'use client'
import { OrganizationSwitcher, UserButton } from '@clerk/nextjs'
export function AppSidebar() {
return (
<aside className="w-64 border-r h-screen flex flex-col">
<div className="p-4 border-b">
<OrganizationSwitcher
hidePersonal={false}
afterCreateOrganizationUrl="/org/:slug/dashboard"
afterSelectOrganizationUrl="/org/:slug/dashboard"
afterSelectPersonalUrl="/dashboard"
appearance={{
elements: {
organizationSwitcherTrigger: "w-full justify-between",
},
}}
/>
</div>
<nav className="flex-1 p-4 space-y-1">
<NavItem href="/dashboard">Übersicht</NavItem>
<NavItem href="/projects">Projekte</NavItem>
<NavItem href="/settings">Einstellungen</NavItem>
</nav>
<div className="p-4 border-t">
<UserButton afterSignOutUrl="/" />
</div>
</aside>
)
}
Organisation erstellen & Mitglieder einladen (API)
// app/api/org/create/route.ts
import { auth, clerkClient } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const { userId } = await auth()
if (!userId) {
return NextResponse.json({ error: 'Nicht authentifiziert' }, { status: 401 })
}
const { name, slug } = await request.json()
const client = await clerkClient()
// Organisation erstellen
const organization = await client.organizations.createOrganization({
name,
slug,
createdBy: userId,
})
return NextResponse.json({ organization }, { status: 201 })
}
// app/api/org/invite/route.ts — Mitglieder einladen
export async function POST(request: Request) {
const { userId, orgId, orgRole } = await auth()
if (!userId || !orgId || orgRole !== 'org:admin') {
return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
}
const { emailAddress, role } = await request.json()
const client = await clerkClient()
const invitation = await client.organizations.createOrganizationInvitation({
organizationId: orgId,
emailAddress,
role: role ?? 'org:member',
inviterUserId: userId,
redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/accept-invitation`,
})
return NextResponse.json({ invitation }, { status: 201 })
}
💡 Claude Code Prompt für Multi-Tenancy: "Implementiere ein vollständiges Multi-Tenant-System mit Clerk Organizations. Jede Organisation soll eigene Projekte, eigene Mitglieder und rollenbasierte Berechtigungen (Admin, Member, Viewer) haben. Datenbankschema mit Prisma."
5. Webhooks & User-Sync mit der Datenbank
Clerk-Events per Webhook in deine Datenbank synchronisieren ist kritisch für produktionsreife Anwendungen. Claude Code generiert vollständige Webhook-Handler inklusive Signatur-Validierung mit Svix — sicher und produktionsbereit.
Webhooks
Svix
User Sync
Webhook-Handler mit Svix-Signatur-Validierung
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix'
import { headers } from 'next/headers'
import { WebhookEvent } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET
export async function POST(request: Request) {
if (!WEBHOOK_SECRET) {
throw new Error('CLERK_WEBHOOK_SECRET nicht gesetzt')
}
// Svix-Header lesen
const headerPayload = await headers()
const svixId = headerPayload.get('svix-id')
const svixTimestamp = headerPayload.get('svix-timestamp')
const svixSignature = headerPayload.get('svix-signature')
if (!svixId || !svixTimestamp || !svixSignature) {
return NextResponse.json({ error: 'Fehlende Svix-Header' }, { status: 400 })
}
// Payload lesen und Signatur verifizieren
const payload = await request.text()
const wh = new Webhook(WEBHOOK_SECRET)
let event: WebhookEvent
try {
event = wh.verify(payload, {
'svix-id': svixId,
'svix-timestamp': svixTimestamp,
'svix-signature': svixSignature,
}) as WebhookEvent
} catch (err) {
console.error('Webhook-Verifikation fehlgeschlagen:', err)
return NextResponse.json({ error: 'Ungültige Signatur' }, { status: 401 })
}
// Events verarbeiten
await handleWebhookEvent(event)
return NextResponse.json({ received: true })
}
User-Events: created, updated, deleted
// lib/webhook-handlers.ts
import { WebhookEvent } from '@clerk/nextjs/server'
import { db } from '@/lib/db'
export async function handleWebhookEvent(event: WebhookEvent) {
switch (event.type) {
case 'user.created': {
const { id, email_addresses, first_name, last_name, image_url, public_metadata } = event.data
await db.user.create({
data: {
clerkId: id,
email: email_addresses[0]?.email_address ?? '',
firstName: first_name ?? '',
lastName: last_name ?? '',
avatarUrl: image_url,
role: (public_metadata as { role?: string })?.role ?? 'user',
plan: 'free',
},
})
// Willkommens-Email auslösen
await sendWelcomeEmail(email_addresses[0]?.email_address ?? '', first_name ?? 'dort')
break
}
case 'user.updated': {
const { id, email_addresses, first_name, last_name, image_url } = event.data
await db.user.update({
where: { clerkId: id },
data: {
email: email_addresses[0]?.email_address,
firstName: first_name ?? '',
lastName: last_name ?? '',
avatarUrl: image_url,
updatedAt: new Date(),
},
})
break
}
case 'user.deleted': {
const { id } = event.data
await db.$transaction([
db.project.deleteMany({ where: { ownerId: id! } }),
db.session.deleteMany({ where: { userId: id! } }),
db.user.delete({ where: { clerkId: id! } }),
])
break
}
case 'organization.created': {
const { id, name, slug, image_url } = event.data
await db.organization.create({
data: {
clerkOrgId: id,
name,
slug: slug ?? id,
logoUrl: image_url,
plan: 'team',
},
})
break
}
default:
console.log(`Unbehandeltes Event: ${event.type}`)
}
}
⚠️ Webhook-Secret: Den CLERK_WEBHOOK_SECRET findest du im Clerk Dashboard unter "Webhooks". Niemals hardcoden — immer aus der .env-Datei laden.
Metadata über clerkClient aktualisieren
// Nach einem Stripe-Checkout: Plan in Clerk-Metadata speichern
import { clerkClient } from '@clerk/nextjs/server'
async function updateUserPlan(clerkUserId: string, plan: 'free' | 'pro' | 'enterprise') {
const client = await clerkClient()
await client.users.updateUserMetadata(clerkUserId, {
publicMetadata: {
plan,
planUpdatedAt: new Date().toISOString(),
},
})
}
6. Custom Claims & RBAC
Custom Claims erlauben es, zusätzliche Daten direkt im JWT-Token zu speichern — ohne zusätzliche Datenbankabfragen bei jedem Request. Kombiniert mit Clerks has()-API und protect() entsteht ein vollständiges RBAC-System. Claude Code generiert diese Patterns auf Basis deiner Rollen-Struktur.
Custom Claims
RBAC
protect()
has()
JWT Template in Clerk Dashboard konfigurieren
// Clerk Dashboard → JWT Templates → Default (Shortcode)
// Füge folgendes zur "Claims" Section hinzu:
{
"metadata": "{{user.public_metadata}}",
"org_metadata": "{{org.public_metadata}}"
}
// Damit landen publicMetadata direkt im JWT:
// sessionClaims.metadata.role, sessionClaims.metadata.plan etc.
Custom Claims in der Middleware lesen
// middleware.ts — mit Custom Claims
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
// TypeScript-Typen für Custom Claims
interface CustomClaims {
metadata?: {
role?: 'user' | 'admin' | 'superadmin'
plan?: 'free' | 'pro' | 'enterprise'
features?: string[]
}
}
const isProRoute = createRouteMatcher(['/pro(.*)', '/api/pro/(.*)'])
const isAdminRoute = createRouteMatcher(['/admin(.*)'])
export default clerkMiddleware(async (auth, request) => {
const { userId, sessionClaims } = await auth()
const claims = sessionClaims as CustomClaims
if (isProRoute(request)) {
if (!userId) return auth.redirectToSignIn()
const plan = claims?.metadata?.plan
if (plan === 'free' || !plan) {
return NextResponse.redirect(new URL('/upgrade', request.url))
}
}
if (isAdminRoute(request)) {
if (!userId) return auth.redirectToSignIn()
const role = claims?.metadata?.role
if (role !== 'admin' && role !== 'superadmin') {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
}
})
has() für Permission-Checks im Client
// components/FeatureGate.tsx
'use client'
import { useAuth } from '@clerk/nextjs'
export function ProFeatureGate({ children }: { children: React.ReactNode }) {
const { has, isLoaded } = useAuth()
if (!isLoaded) return <LoadingSpinner />
// has() prüft Clerk Permissions (im Dashboard konfiguriert)
const canAccessPro = has?.({ permission: 'org:feature:pro_access' })
const isOrgAdmin = has?.({ role: 'org:admin' })
if (!canAccessPro) {
return (
<div className="p-6 bg-amber-50 border border-amber-200 rounded-lg">
<p>Diese Funktion ist nur im Pro-Plan verfügbar.</p>
<a href="/upgrade">Jetzt upgraden →</a>
</div>
)
}
return <>{children}</>
}
protect() in Server Actions
// app/actions/admin.ts
'use server'
import { auth } from '@clerk/nextjs/server'
export async function deleteUser(targetUserId: string) {
// protect() wirft automatisch eine Unauthorized-Exception wenn Bedingung nicht erfüllt
const { userId } = await auth.protect(has => {
return has({ role: 'org:admin' }) || has({ permission: 'org:sys:manage_users' })
})
if (userId === targetUserId) {
throw new Error('Du kannst dich nicht selbst löschen')
}
await db.user.delete({ where: { clerkId: targetUserId } })
}
export async function generateReport() {
// Nur für Superadmins via Custom Claims
const { sessionClaims } = await auth()
const role = (sessionClaims as { metadata?: { role?: string } })?.metadata?.role
if (role !== 'superadmin') {
throw new Error('Keine Berechtigung')
}
return generateAdminReport()
}
Admin-Dashboard mit rollenbasierter UI
// app/admin/page.tsx — Server Component
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
type AdminRole = 'admin' | 'superadmin'
export default async function AdminPage() {
const { sessionClaims, userId } = await auth()
if (!userId) redirect('/sign-in')
const metadata = sessionClaims?.metadata as { role?: AdminRole; plan?: string }
const role = metadata?.role
if (role !== 'admin' && role !== 'superadmin') {
redirect('/unauthorized')
}
const isSuperAdmin = role === 'superadmin'
const [users, stats] = await Promise.all([
db.user.findMany({ orderBy: { createdAt: 'desc' }, take: 50 }),
db.user.aggregate({ _count: true }),
])
return (
<div className="p-8">
<h1>Admin Dashboard</h1>
<p>Gesamt: {stats._count} Nutzer</p>
<UserTable users={users} canDelete={isSuperAdmin} />
{isSuperAdmin && <SystemSettings />}
</div>
)
}
💡 RBAC Best Practice: Kombiniere Clerk Organizations Roles (org:admin) für team-basierte Berechtigungen mit Custom Claims (publicMetadata.role) für anwendungsweite Rollen. Claude Code kann dir helfen, die richtige Kombination für dein Use Case zu finden.
Auth-Modul im Kurs
Im Claude Code Mastery Kurs: vollständiges Clerk-Modul mit Organizations, Webhooks, Custom Claims und RBAC für produktionsreife Next.js-Anwendungen.
14 Tage kostenlos testen →