api-auth-clerk
Clerk managed authentication - ClerkProvider, middleware, pre-built components, hooks, server-side auth, organizations, webhooks
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-auth-clerk/skills/api-auth-clerk
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Clerk Authentication Patterns
Quick Guide: Clerk provides managed authentication with pre-built UI components, server-side helpers, and organization-based multi-tenancy. Use
clerkMiddleware()for route protection,<Show>for conditional rendering, hooks for client state, andauth()/currentUser()for server-side auth. Clerk Core 3 (2026) replaces<SignedIn>/<SignedOut>with<Show>, renames the middleware file toproxy.ts(Next.js 16+), and consolidates packages.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use @clerk/nextjs/server for ALL server-side imports -- NEVER import server helpers from @clerk/nextjs)
(You MUST verify webhooks using Clerk's verifyWebhook helper -- NEVER trust unverified webhook payloads)
(You MUST use <Show> component instead of deprecated <SignedIn>/<SignedOut>/<Protect> -- these are removed in Core 3)
(You MUST NOT pass the full currentUser() object to the client -- it contains privateMetadata that must stay server-side)
(You MUST protect routes in BOTH middleware AND data access layer -- middleware alone is insufficient)
</critical_requirements>
Auto-detection: Clerk, ClerkProvider, clerkMiddleware, @clerk/nextjs, useUser, useAuth, useClerk, useSession, useOrganization, SignIn, SignUp, UserButton, UserProfile, OrganizationSwitcher, auth(), currentUser(), CLERK_SECRET_KEY, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, Show when="signed-in"
When to use:
- Adding authentication and user management to an application
- Building multi-tenant B2B apps with organization-based access control
- Using pre-built sign-in/sign-up UI components with customizable theming
- Protecting routes with middleware and server-side authorization checks
- Syncing Clerk user data to your database via webhooks
Key patterns covered:
- ClerkProvider setup, environment variables, middleware configuration
- Pre-built UI components (
<SignIn>,<SignUp>,<UserButton>,<Show>) - Client-side hooks (
useUser,useAuth,useSession,useOrganization) - Server-side auth (
auth(),currentUser()) in Server Components, Route Handlers, Server Actions - Middleware route protection with
clerkMiddleware()andcreateRouteMatcher() - Organization-based multi-tenancy with roles and permissions
- Webhook handling with Svix signature verification
When NOT to use:
- Self-hosted auth requirement (need full control over auth data storage)
- Cannot use a third-party auth service (compliance/regulatory constraints)
- Simple API key authentication (custom middleware is sufficient)
- Budget constraints prevent using a managed service
Detailed Resources:
- reference.md - Decision frameworks, hooks quick reference, Core 3 migration cheat sheet
- examples/core.md - ClerkProvider, environment variables, middleware configuration
- examples/components.md - Pre-built components, customization, appearance prop
- examples/hooks.md - useUser, useAuth, useSession, loading states, conditional rendering
- examples/server.md - Server Components, API routes, Server Actions, webhook handling
- examples/organizations.md - Organization management, roles, permissions, RBAC
<decision_framework>
Decision Framework
Client vs Server Auth
Where do you need auth data?
|-- Server Component, Route Handler, Server Action
| |-- Need just userId/sessionId? --> auth()
| |-- Need full user object? --> currentUser()
| |-- Need to protect the route? --> auth.protect()
| +-- Need org context? --> auth() returns orgId, orgRole
|
+-- Client Component (interactive UI)
|-- Need user profile data? --> useUser()
|-- Need session/token data? --> useAuth()
|-- Need org data? --> useOrganization()
+-- Need low-level Clerk API? --> useClerk()
Route Protection Strategy
What kind of route is it?
|-- Public (landing, sign-in, sign-up, webhooks)
| +-- Add to isPublicRoute matcher, skip auth.protect()
|
|-- Authenticated (dashboard, profile, settings)
| +-- auth.protect() in middleware + auth() check in data layer
|
+-- Authorized (admin, org-specific, permission-gated)
|-- Role-based? --> auth.protect({ role: "org:admin" })
+-- Permission-based? --> auth.protect((has) => has({ permission: "org:feature:action" }))
Component Choice
What auth UI do you need?
|-- Full sign-in page --> <SignIn /> on catch-all route
|-- Full sign-up page --> <SignUp /> on catch-all route
|-- Sign-in button (modal) --> <SignInButton />
|-- User avatar + menu --> <UserButton />
|-- Full profile editor --> <UserProfile />
|-- Org switcher --> <OrganizationSwitcher />
+-- Conditional content --> <Show when="signed-in"> or <Show when={{ role: "..." }}>
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Importing server helpers from
@clerk/nextjsinstead of@clerk/nextjs/server(breaks in Server Components) - Using deprecated
<SignedIn>/<SignedOut>/<Protect>components (removed in Core 3) - Passing full
currentUser()object to client components (leaksprivateMetadata) - Trusting webhook payloads without
verifyWebhooksignature verification - Relying solely on middleware for route protection (middleware can be bypassed)
- Importing types from
@clerk/typesinstead of@clerk/shared/types(Core 3 rename)
Medium Priority Issues:
- Not checking
isLoadedbefore accessing hook data (causes hydration errors) - Using
currentUser()on the client side (it is server-only) - Hardcoding Clerk keys instead of using environment variables
- Using
authMiddleware()(deprecated, replaced byclerkMiddleware()) - Not making webhook endpoint public in middleware matcher
- Using
CLERK_WEBHOOK_SECRETinstead ofCLERK_WEBHOOK_SIGNING_SECRET
Common Mistakes:
- Naming middleware file
middleware.tson Next.js 16+ (should beproxy.ts) orproxy.tson Next.js <=15 (should bemiddleware.ts) - Forgetting catch-all segments
[[...sign-in]]on sign-in/sign-up pages (breaks multi-step flows) - Using
getToken()without try/catch in Core 3 (throwsClerkOfflineErrorwhen offline instead of returning null) - Not adding
prefetch={false}to<Link>components pointing at protected routes from public pages
Gotchas & Edge Cases:
currentUser()counts against Backend API rate limits -- preferuseUser()hook on the client when possibleauth()in Server Components is deduplicated per request (safe to call multiple times)<Show when={{ role: "org:admin" }}>requires an active organization in the session- Organization roles use the
org:prefix (e.g.,org:admin,org:member,org:billing) - Clerk Core 3 requires Node.js 20.9.0+, Next.js 15.2.3+
@clerk/clerk-reactrenamed to@clerk/reactin Core 3 -- update imports after upgrade
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use @clerk/nextjs/server for ALL server-side imports -- NEVER import server helpers from @clerk/nextjs)
(You MUST verify webhooks using Clerk's verifyWebhook helper -- NEVER trust unverified webhook payloads)
(You MUST use <Show> component instead of deprecated <SignedIn>/<SignedOut>/<Protect> -- these are removed in Core 3)
(You MUST NOT pass the full currentUser() object to the client -- it contains privateMetadata that must stay server-side)
(You MUST protect routes in BOTH middleware AND data access layer -- middleware alone is insufficient)
Failure to follow these rules will create authentication vulnerabilities or break on Clerk Core 3.
</critical_reminders>
Files (skills)
-
examples
-
components.md 9.1 KB
# Clerk Pre-Built Components Examples > UI components, customization, and the appearance prop. See [SKILL.md](../SKILL.md) for core concepts. **Core setup:** See [core.md](core.md). **Client hooks:** See [hooks.md](hooks.md). **Server auth:** See [server.md](server.md). --- ## Pattern 1: Show Component (Conditional Rendering) The `<Show>` component (Core 3) replaces deprecated `<SignedIn>`, `<SignedOut>`, and `<Protect>`. ### Good Example -- Auth-Aware Navigation ```tsx // components/nav-bar.tsx "use client"; import { Show, SignInButton, SignUpButton, UserButton, OrganizationSwitcher, } from "@clerk/nextjs"; import Link from "next/link"; export function NavBar() { return ( <nav className="nav-bar"> <Link href="/">Home</Link> <Show when="signed-in"> <Link href="/dashboard">Dashboard</Link> <OrganizationSwitcher /> <UserButton /> </Show> <Show when="signed-out"> <SignInButton mode="modal" /> <SignUpButton mode="modal" /> </Show> </nav> ); } ``` **Why good:** `<Show>` for conditional rendering, modal mode opens sign-in overlay (no page navigation), `<UserButton>` provides profile/sign-out menu, named export ### Good Example -- Role-Based UI ```tsx // components/admin-nav.tsx "use client"; import { Show } from "@clerk/nextjs"; import Link from "next/link"; export function AdminNav() { return ( <nav> {/* Visible to all signed-in users */} <Show when="signed-in"> <Link href="/dashboard">Dashboard</Link> </Show> {/* Visible only to org admins */} <Show when={{ role: "org:admin" }}> <Link href="/admin">Admin Panel</Link> <Link href="/admin/members">Manage Members</Link> </Show> {/* Permission-based rendering */} <Show when={{ permission: "org:billing:manage" }}> <Link href="/billing">Billing</Link> </Show> {/* Callback-based condition */} <Show when={(has) => has({ role: "org:admin" }) || has({ permission: "org:reports:read" }) } fallback={<p>You do not have access to reports.</p>} > <Link href="/reports">Reports</Link> </Show> </nav> ); } ``` **Why good:** Multiple `<Show>` conditions (string, object, callback), fallback for unauthorized users, combines role and permission checks ### Bad Example -- Using Deprecated Components ```tsx // BAD: Removed in Core 3 import { SignedIn, SignedOut, Protect } from "@clerk/nextjs"; export default function Nav() { return ( <> <SignedIn> <Link href="/dashboard">Dashboard</Link> </SignedIn> <SignedOut> <SignInButton /> </SignedOut> <Protect role="admin" fallback={<p>No access</p>}> <Link href="/admin">Admin</Link> </Protect> </> ); } ``` **Why bad:** `<SignedIn>`, `<SignedOut>`, `<Protect>` removed in Core 3, must migrate to `<Show>` with `when` prop --- ## Pattern 2: UserButton and UserProfile ### Good Example -- UserButton with Custom Menu Items ```tsx // components/user-menu.tsx "use client"; import { UserButton } from "@clerk/nextjs"; export function UserMenu() { return ( <UserButton> <UserButton.MenuItems> <UserButton.Link label="My Orders" labelIcon={<ShoppingCartIcon />} href="/orders" /> <UserButton.Link label="Billing" labelIcon={<CreditCardIcon />} href="/billing" /> <UserButton.Action label="manageAccount" /> <UserButton.Action label="signOut" /> </UserButton.MenuItems> </UserButton> ); } ``` **Why good:** Custom menu items alongside built-in actions, `manageAccount` opens Clerk profile manager, type-safe built-in action labels ### Good Example -- UserProfile on Dedicated Page ```tsx // app/settings/[[...settings]]/page.tsx import { UserProfile } from "@clerk/nextjs"; export function SettingsPage() { return ( <main className="settings-page"> <h1>Account Settings</h1> <UserProfile /> </main> ); } ``` **Why good:** Catch-all route handles Clerk's multi-tab navigation, full profile management UI (name, email, password, security, connected accounts) --- ## Pattern 3: Appearance Prop Customization ### Good Example -- Global Theme via ClerkProvider ```tsx // app/layout.tsx import { ClerkProvider } from "@clerk/nextjs"; import { dark } from "@clerk/themes"; const BRAND_COLOR = "#6366f1"; const BORDER_RADIUS = "0.75rem"; export function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <ClerkProvider appearance={{ baseTheme: dark, variables: { colorPrimary: BRAND_COLOR, borderRadius: BORDER_RADIUS, fontFamily: "Inter, sans-serif", }, elements: { // Target specific internal elements formButtonPrimary: "bg-indigo-600 hover:bg-indigo-700 text-white", card: "shadow-lg border border-gray-200", headerTitle: "text-2xl font-bold", }, }} > {children} </ClerkProvider> </body> </html> ); } ``` **Why good:** Named constants for brand values, `variables` for broad theme changes, `elements` for specific component targeting, dark theme as base, applies to all Clerk components ### Good Example -- Per-Component Appearance Override ```tsx // app/sign-in/[[...sign-in]]/page.tsx import { SignIn } from "@clerk/nextjs"; const SIGN_IN_APPEARANCE = { elements: { rootBox: "mx-auto max-w-md", card: "rounded-xl shadow-2xl", headerTitle: "text-3xl font-bold text-center", headerSubtitle: "text-gray-500 text-center", socialButtonsBlockButton: "rounded-lg border-2", }, } as const; export function SignInPage() { return ( <main className="flex min-h-screen items-center justify-center"> <SignIn appearance={SIGN_IN_APPEARANCE} /> </main> ); } ``` **Why good:** Per-component override (doesn't affect other Clerk components), extracted to named constant, `as const` for type safety, centered layout ### Good Example -- Stacking Multiple Themes ```tsx // app/layout.tsx import { ClerkProvider } from "@clerk/nextjs"; import { dark, neobrutalism } from "@clerk/themes"; export function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <ClerkProvider appearance={{ baseTheme: [dark, neobrutalism], variables: { colorPrimary: "#ff6b6b", }, }} > {children} </ClerkProvider> </body> </html> ); } ``` **Why good:** Themes stack in order (last wins for conflicts), custom variables applied on top, easy to swap themes --- ## Pattern 4: SignIn and SignUp with Routing ### Good Example -- Modal Mode ```tsx // components/auth-buttons.tsx "use client"; import { SignInButton, SignUpButton } from "@clerk/nextjs"; export function AuthButtons() { return ( <div className="auth-buttons"> <SignInButton mode="modal"> <button className="btn btn-primary">Sign In</button> </SignInButton> <SignUpButton mode="modal"> <button className="btn btn-secondary">Create Account</button> </SignUpButton> </div> ); } ``` **Why good:** Modal mode keeps user on current page, custom button styling by wrapping with your own element, no page navigation required ### Good Example -- Redirect Mode with Custom Routing ```tsx // components/auth-buttons.tsx "use client"; import { SignInButton, SignUpButton } from "@clerk/nextjs"; const SIGN_IN_REDIRECT = "/dashboard"; const SIGN_UP_REDIRECT = "/onboarding"; export function AuthButtons() { return ( <div className="auth-buttons"> <SignInButton mode="redirect" forceRedirectUrl={SIGN_IN_REDIRECT}> <button className="btn btn-primary">Sign In</button> </SignInButton> <SignUpButton mode="redirect" forceRedirectUrl={SIGN_UP_REDIRECT}> <button className="btn btn-secondary">Get Started</button> </SignUpButton> </div> ); } ``` **Why good:** Named constants for redirect URLs, redirect mode navigates to dedicated auth pages, `forceRedirectUrl` overrides default redirect --- ## Pattern 5: OrganizationSwitcher ### Good Example -- Switcher with Creation ```tsx // components/org-switcher.tsx "use client"; import { OrganizationSwitcher } from "@clerk/nextjs"; export function OrgSwitcher() { return ( <OrganizationSwitcher hidePersonal afterCreateOrganizationUrl="/org/:slug" afterSelectOrganizationUrl="/org/:slug" appearance={{ elements: { rootBox: "w-full", organizationSwitcherTrigger: "w-full justify-between rounded-lg border p-2", }, }} /> ); } ``` **Why good:** `hidePersonal` removes personal workspace option (B2B apps), `:slug` placeholder in URLs auto-replaced with org slug, custom styling for full-width trigger --- _For client hooks, see [hooks.md](hooks.md). For server-side auth, see [server.md](server.md). For organizations, see [organizations.md](organizations.md)._ -
core.md 8.8 KB
# Clerk Core Setup Examples > Provider setup, environment variables, and middleware configuration. See [SKILL.md](../SKILL.md) for core concepts. **UI components:** See [components.md](components.md). **Client hooks:** See [hooks.md](hooks.md). **Server auth:** See [server.md](server.md). **Organizations:** See [organizations.md](organizations.md). --- ## Pattern 1: Environment Variables ### Good Example -- Complete Environment Setup ```env # .env.local # Required: Clerk API keys (from Dashboard > Configure > API Keys) NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_abc123... CLERK_SECRET_KEY=sk_test_xyz789... # Optional: Custom auth page paths NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up # Optional: Post-auth redirect destinations NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/dashboard NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/onboarding # Optional: Webhook signing secret (from Dashboard > Webhooks > Endpoint) CLERK_WEBHOOK_SIGNING_SECRET=whsec_abc123... ``` **Why good:** All keys sourced from environment variables, public keys prefixed with `NEXT_PUBLIC_`, secret keys server-only, webhook secret separate from API keys ### Bad Example -- Hardcoded Keys ```tsx // BAD: Keys in source code <ClerkProvider publishableKey="pk_test_abc123">{children}</ClerkProvider> ``` **Why bad:** Secrets in source code get committed to version control, no environment separation between dev/staging/prod --- ## Pattern 2: ClerkProvider Setup ### Good Example -- Minimal Provider ```tsx // app/layout.tsx import { ClerkProvider } from "@clerk/nextjs"; export function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <ClerkProvider>{children}</ClerkProvider> </body> </html> ); } ``` **Why good:** Provider inside `<body>` (not wrapping `<html>`), reads publishable key from env automatically, named export, minimal config ### Good Example -- Provider with Appearance and Localization ```tsx // app/layout.tsx import { ClerkProvider } from "@clerk/nextjs"; import { dark } from "@clerk/themes"; export function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <ClerkProvider appearance={{ baseTheme: dark, variables: { colorPrimary: "#3b82f6", borderRadius: "0.5rem", }, }} localization={{ signIn: { start: { title: "Welcome back", subtitle: "Sign in to your account", }, }, }} > {children} </ClerkProvider> </body> </html> ); } ``` **Why good:** Theme applied globally to all Clerk components, CSS variables for brand consistency, localization for custom copy ### Good Example -- Dynamic Provider for Client-Side Auth ```tsx // app/layout.tsx import { ClerkProvider } from "@clerk/nextjs"; export function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <ClerkProvider dynamic>{children}</ClerkProvider> </body> </html> ); } ``` **Why good:** `dynamic` prop required when routes need runtime authentication access with Next.js static rendering --- ## Pattern 3: Middleware Configuration ### Good Example -- Protect All Routes Except Public ```ts // proxy.ts (Next.js 16+) or middleware.ts (Next.js <=15) import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; const isPublicRoute = createRouteMatcher([ "/", "/sign-in(.*)", "/sign-up(.*)", "/api/webhooks(.*)", "/about", "/pricing", ]); export default clerkMiddleware(async (auth, req) => { if (!isPublicRoute(req)) { await auth.protect(); } }); export const config = { matcher: [ // Skip Next.js internals and all static files "/((?!_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)(.*)", ], }; ``` **Why good:** Explicit public route list, everything else requires auth, webhook endpoints public (verified separately), standard matcher pattern excludes static assets ### Good Example -- Role-Based Route Protection ```ts // proxy.ts import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; const isPublicRoute = createRouteMatcher([ "/", "/sign-in(.*)", "/sign-up(.*)", "/api/webhooks(.*)", ]); const isAdminRoute = createRouteMatcher(["/admin(.*)"]); const isBillingRoute = createRouteMatcher(["/billing(.*)"]); export default clerkMiddleware(async (auth, req) => { // Admin routes: require org:admin role if (isAdminRoute(req)) { await auth.protect((has) => has({ role: "org:admin" })); return; } // Billing routes: require billing permission if (isBillingRoute(req)) { await auth.protect((has) => has({ permission: "org:billing:manage" })); return; } // All other non-public routes: require authentication if (!isPublicRoute(req)) { 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)(.*)", ], }; ``` **Why good:** Tiered protection (admin > billing > authenticated), role and permission checks at middleware level, early return after specific checks ### Good Example -- Combining with Other Middleware ```ts // proxy.ts import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; // Import your other middleware (i18n, rate limiting, etc.) import { otherMiddleware } from "./lib/middleware"; const isPublicRoute = createRouteMatcher(["/", "/sign-in(.*)", "/sign-up(.*)"]); export default clerkMiddleware(async (auth, req) => { if (!isPublicRoute(req)) { await auth.protect(); } // Chain other middleware after Clerk auth return otherMiddleware(req); }); 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)(.*)", ], }; ``` **Why good:** Clerk middleware wraps other middleware, auth runs first then additional middleware, return value from clerkMiddleware callback becomes the response ### Bad Example -- No Route Protection ```ts // BAD: clerkMiddleware with no protection import { clerkMiddleware } from "@clerk/nextjs/server"; export default clerkMiddleware(); export const config = { matcher: ["/((?!_next).*)", "/(api|trpc)(.*)"], }; ``` **Why bad:** `clerkMiddleware()` without callback leaves ALL routes public by default, auth data is available but routes are not protected ### Bad Example -- Using Deprecated authMiddleware ```ts // BAD: Deprecated API import { authMiddleware } from "@clerk/nextjs"; export default authMiddleware({ publicRoutes: ["/", "/sign-in", "/sign-up"], }); ``` **Why bad:** `authMiddleware` is deprecated and removed in recent versions, replaced by `clerkMiddleware` with `createRouteMatcher` --- ## Pattern 4: Sign-In and Sign-Up Pages ### Good Example -- Dedicated Auth Pages ```tsx // app/sign-in/[[...sign-in]]/page.tsx import { SignIn } from "@clerk/nextjs"; export function SignInPage() { return ( <main className="auth-page"> <SignIn /> </main> ); } ``` ```tsx // app/sign-up/[[...sign-up]]/page.tsx import { SignUp } from "@clerk/nextjs"; export function SignUpPage() { return ( <main className="auth-page"> <SignUp /> </main> ); } ``` **Why good:** Catch-all route `[[...sign-in]]` handles multi-step flows (MFA, OAuth callbacks), named exports, minimal wrapper lets Clerk handle the form ### Bad Example -- Missing Catch-All Segment ```tsx // BAD: app/sign-in/page.tsx (no catch-all) import { SignIn } from "@clerk/nextjs"; export default function SignInPage() { return <SignIn />; } ``` **Why bad:** Without `[[...sign-in]]` catch-all, multi-step auth flows (MFA verification, OAuth callbacks) return 404, default export --- ## Pattern 5: Debugging Middleware ### Good Example -- Debug Mode ```ts // proxy.ts import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; const isPublicRoute = createRouteMatcher(["/"]); export default clerkMiddleware( async (auth, req) => { if (!isPublicRoute(req)) { await auth.protect(); } }, { debug: process.env.NODE_ENV === "development" }, ); 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)(.*)", ], }; ``` **Why good:** Debug logging only in development, helps diagnose route protection and auth state issues, no performance impact in production --- _For pre-built components, see [components.md](components.md). For client hooks, see [hooks.md](hooks.md). For server-side auth, see [server.md](server.md)._ -
hooks.md 9.6 KB
# Clerk Client-Side Hooks Examples > Client-side hooks, conditional rendering, and loading states. See [SKILL.md](../SKILL.md) for core concepts. **Core setup:** See [core.md](core.md). **Components:** See [components.md](components.md). **Server auth:** See [server.md](server.md). --- ## Pattern 1: useUser -- User Profile Data ### Good Example -- User Profile Display ```tsx // components/user-profile-card.tsx "use client"; import { useUser } from "@clerk/nextjs"; export function UserProfileCard() { const { isLoaded, isSignedIn, user } = useUser(); if (!isLoaded) { return <div className="skeleton" aria-label="Loading user profile" />; } if (!isSignedIn) { return <p>Please sign in to view your profile.</p>; } const primaryEmail = user.emailAddresses.find( (e) => e.id === user.primaryEmailAddressId, )?.emailAddress; return ( <div className="profile-card"> <img src={user.imageUrl} alt={`${user.firstName}'s avatar`} /> <h2> {user.firstName} {user.lastName} </h2> <p>{primaryEmail}</p> <p>Joined: {user.createdAt?.toLocaleDateString()}</p> </div> ); } ``` **Why good:** Checks `isLoaded` first (prevents undefined access), checks `isSignedIn` before accessing `user`, finds primary email from email addresses array, accessible loading state ### Good Example -- Update User Profile ```tsx // components/edit-name-form.tsx "use client"; import { useUser } from "@clerk/nextjs"; import { useState, type FormEvent } from "react"; export function EditNameForm() { const { isLoaded, isSignedIn, user } = useUser(); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); const [saving, setSaving] = useState(false); if (!isLoaded || !isSignedIn) return null; async function handleSubmit(e: FormEvent) { e.preventDefault(); setSaving(true); try { await user.update({ firstName, lastName }); } finally { setSaving(false); } } return ( <form onSubmit={handleSubmit}> <input value={firstName} onChange={(e) => setFirstName(e.target.value)} placeholder={user.firstName ?? "First name"} /> <input value={lastName} onChange={(e) => setLastName(e.target.value)} placeholder={user.lastName ?? "Last name"} /> <button type="submit" disabled={saving}> {saving ? "Saving..." : "Update Name"} </button> </form> ); } ``` **Why good:** `user.update()` method updates Clerk user data directly, loading state during save, placeholder shows current values, guards against unloaded state ### Bad Example -- No Loading Check ```tsx // BAD: Accessing user without guards "use client"; import { useUser } from "@clerk/nextjs"; export default function Profile() { const { user } = useUser(); // user is undefined until isLoaded is true, null when signed out return <p>Hello {user.firstName}</p>; // Runtime error! } ``` **Why bad:** `user` is `undefined` during initialization and `null` when signed out, accessing `.firstName` throws TypeError, no loading state for UX, default export --- ## Pattern 2: useAuth -- Session and Authorization ### Good Example -- Authenticated API Calls ```tsx // components/data-fetcher.tsx "use client"; import { useAuth } from "@clerk/nextjs"; import { useCallback, useEffect, useState } from "react"; interface DashboardData { stats: { label: string; value: number }[]; } export function DataFetcher() { const { isLoaded, isSignedIn, getToken } = useAuth(); const [data, setData] = useState<DashboardData | null>(null); const [error, setError] = useState<string | null>(null); const fetchData = useCallback(async () => { try { const token = await getToken(); const response = await fetch("/api/dashboard", { headers: { Authorization: `Bearer ${token}` }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } setData(await response.json()); } catch (err) { // Core 3: getToken() throws ClerkOfflineError when offline setError(err instanceof Error ? err.message : "Failed to fetch"); } }, [getToken]); useEffect(() => { if (isSignedIn) { fetchData(); } }, [isSignedIn, fetchData]); if (!isLoaded) return <div>Loading auth...</div>; if (!isSignedIn) return <div>Sign in required</div>; if (error) return <div>Error: {error}</div>; if (!data) return <div>Loading data...</div>; return ( <ul> {data.stats.map((stat) => ( <li key={stat.label}> {stat.label}: {stat.value} </li> ))} </ul> ); } ``` **Why good:** `getToken()` provides JWT for API authentication, try/catch handles `ClerkOfflineError` (Core 3 behavior), sequential loading states, fetches only when signed in ### Good Example -- Authorization Check with has() ```tsx // components/admin-actions.tsx "use client"; import { useAuth } from "@clerk/nextjs"; export function AdminActions() { const { isLoaded, isSignedIn, has, orgRole } = useAuth(); if (!isLoaded || !isSignedIn) return null; const isAdmin = has?.({ role: "org:admin" }); const canManageBilling = has?.({ permission: "org:billing:manage" }); return ( <div className="admin-actions"> <p>Current role: {orgRole ?? "No organization"}</p> {isAdmin && ( <button onClick={() => window.location.assign("/admin")}> Admin Panel </button> )} {canManageBilling && ( <button onClick={() => window.location.assign("/billing")}> Manage Billing </button> )} </div> ); } ``` **Why good:** `has()` checks roles and permissions client-side, optional chaining on `has?.()` (null when no active org), `orgRole` shows current role, named export --- ## Pattern 3: useSession -- Session Management ### Good Example -- Session Info Display ```tsx // components/session-info.tsx "use client"; import { useSession } from "@clerk/nextjs"; export function SessionInfo() { const { isLoaded, isSignedIn, session } = useSession(); if (!isLoaded) return <div>Loading...</div>; if (!isSignedIn || !session) return null; return ( <div className="session-info"> <p>Session ID: {session.id}</p> <p>Last active: {session.lastActiveAt.toLocaleString()}</p> <p>Status: {session.status}</p> <p>Expires: {session.expireAt.toLocaleString()}</p> </div> ); } ``` **Why good:** Session object provides activity and expiration info, useful for session management UIs, guards against unloaded state --- ## Pattern 4: useOrganization -- Active Organization ### Good Example -- Organization Dashboard ```tsx // components/org-dashboard.tsx "use client"; import { useOrganization } from "@clerk/nextjs"; export function OrgDashboard() { const { isLoaded, organization, membership } = useOrganization(); if (!isLoaded) return <div>Loading organization...</div>; if (!organization) { return ( <div> <p>No active organization.</p> <p>Select or create an organization to continue.</p> </div> ); } return ( <div className="org-dashboard"> <div className="org-header"> <img src={organization.imageUrl} alt={organization.name} /> <h1>{organization.name}</h1> <p>Slug: {organization.slug}</p> <p>Members: {organization.membersCount}</p> </div> <div className="membership-info"> <p>Your role: {membership?.role}</p> <p>Joined: {membership?.createdAt?.toLocaleDateString()}</p> </div> </div> ); } ``` **Why good:** Handles no-org state (user might not have selected an org), `membership` contains user's role in active org, `organization` has org metadata --- ## Pattern 5: Combining Multiple Hooks ### Good Example -- Complete Dashboard Header ```tsx // components/dashboard-header.tsx "use client"; import { useUser, useAuth, useOrganization } from "@clerk/nextjs"; import { UserButton, OrganizationSwitcher } from "@clerk/nextjs"; export function DashboardHeader() { const { isLoaded: userLoaded, user } = useUser(); const { orgId } = useAuth(); const { organization } = useOrganization(); if (!userLoaded) { return <header className="dashboard-header skeleton" />; } return ( <header className="dashboard-header"> <div className="header-left"> <h1> {orgId && organization ? organization.name : `${user?.firstName}'s Workspace`} </h1> </div> <div className="header-right"> <OrganizationSwitcher hidePersonal={false} /> <UserButton /> </div> </header> ); } ``` **Why good:** Combines user, auth, and org hooks for complete header, graceful fallback to personal workspace when no org, `hidePersonal={false}` allows personal workspace option --- ## Pattern 6: Reverification for Sensitive Actions ### Good Example -- Reverify Before Dangerous Action ```tsx // components/delete-account.tsx "use client"; import { useReverification, useUser } from "@clerk/nextjs"; export function DeleteAccountButton() { const { user } = useUser(); const deleteAccount = useReverification(async () => { // This callback only runs after the user re-authenticates await user?.delete(); window.location.assign("/"); }); return ( <button onClick={() => deleteAccount()} className="btn btn-danger"> Delete My Account </button> ); } ``` **Why good:** `useReverification` prompts user to re-authenticate before executing, protects sensitive actions from session theft, callback pattern keeps code clean --- _For server-side auth, see [server.md](server.md). For organizations, see [organizations.md](organizations.md)._ -
organizations.md 15.4 KB
# Clerk Organizations & Multi-Tenancy Examples > Organization management, role-based access, permissions, and multi-tenant patterns. See [SKILL.md](../SKILL.md) for core concepts. **Core setup:** See [core.md](core.md). **Server auth:** See [server.md](server.md). **Client hooks:** See [hooks.md](hooks.md). --- ## Pattern 1: Organization Setup and Switching ### Good Example -- Organization-Aware Layout ```tsx // app/(org)/layout.tsx import { auth } from "@clerk/nextjs/server"; import { redirect } from "next/navigation"; export default async function OrgLayout({ children, }: { children: React.ReactNode; }) { const { orgId, isAuthenticated } = await auth(); if (!isAuthenticated) { redirect("/sign-in"); } // Require an active organization for all org routes if (!orgId) { redirect("/org-selection"); } return <div className="org-layout">{children}</div>; } ``` **Why good:** Layout-level org check ensures all nested routes have an active org, redirects to org selection when no org is active, defense in depth on top of middleware ### Good Example -- Organization Selection Page ```tsx // app/org-selection/page.tsx "use client"; import { OrganizationList } from "@clerk/nextjs"; export function OrgSelectionPage() { return ( <main className="org-selection"> <h1>Select an Organization</h1> <OrganizationList afterSelectOrganizationUrl="/dashboard" afterCreateOrganizationUrl="/dashboard" /> </main> ); } ``` **Why good:** `<OrganizationList>` provides create + select UI, redirects to dashboard after selection/creation, simple page for org-required apps ### Good Example -- Organization Switcher in Sidebar ```tsx // components/sidebar.tsx "use client"; import { OrganizationSwitcher } from "@clerk/nextjs"; export function Sidebar() { return ( <aside className="sidebar"> <OrganizationSwitcher hidePersonal afterCreateOrganizationUrl="/dashboard" afterSelectOrganizationUrl="/dashboard" appearance={{ elements: { rootBox: "w-full", organizationSwitcherTrigger: "w-full rounded-lg border p-3 hover:bg-gray-50", }, }} /> <nav>{/* sidebar navigation */}</nav> </aside> ); } ``` **Why good:** `hidePersonal` for B2B apps (no personal workspace), redirect after org change reloads dashboard with new org context, full-width styling --- ## Pattern 2: Role-Based Access Control ### Good Example -- Middleware-Level RBAC ```ts // proxy.ts import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; const isPublicRoute = createRouteMatcher([ "/", "/sign-in(.*)", "/sign-up(.*)", "/api/webhooks(.*)", ]); const isAdminRoute = createRouteMatcher(["/admin(.*)", "/api/admin(.*)"]); const isMemberRoute = createRouteMatcher([ "/dashboard(.*)", "/projects(.*)", "/api/projects(.*)", ]); export default clerkMiddleware(async (auth, req) => { // Admin routes: require org:admin role if (isAdminRoute(req)) { await auth.protect((has) => has({ role: "org:admin" })); return; } // Member routes: require any org membership (any role) if (isMemberRoute(req)) { await auth.protect(); return; } // All other non-public routes: require authentication if (!isPublicRoute(req)) { 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)(.*)", ], }; ``` **Why good:** Tiered protection -- admin routes require admin role, member routes require auth, public routes open, early return prevents falling through to less restrictive checks ### Good Example -- Server Component RBAC ```tsx // app/admin/members/page.tsx import { auth } from "@clerk/nextjs/server"; import { redirect } from "next/navigation"; export default async function MembersPage() { const { orgId, has, isAuthenticated, redirectToSignIn } = await auth(); if (!isAuthenticated) return redirectToSignIn(); if (!orgId) redirect("/org-selection"); // Server-side role check (defense in depth) if (!has({ role: "org:admin" })) { redirect("/dashboard"); } const members = await db.query.orgMembers.findMany({ where: eq(orgMembers.orgId, orgId), }); return ( <main> <h1>Organization Members</h1> <table> <thead> <tr> <th>Name</th> <th>Email</th> <th>Role</th> </tr> </thead> <tbody> {members.map((member) => ( <tr key={member.id}> <td>{member.name}</td> <td>{member.email}</td> <td>{member.role}</td> </tr> ))} </tbody> </table> </main> ); } ``` **Why good:** Auth, org, and role checks at data layer (not just middleware), `orgId` scopes query, custom redirect for unauthorized (to dashboard, not 404) --- ## Pattern 3: Permission-Based Access Control ### Good Example -- Custom Permissions Custom permissions follow the format `org:<feature>:<action>`. Configure in Clerk Dashboard under Configure > Organizations > Roles. ```tsx // app/invoices/page.tsx import { auth } from "@clerk/nextjs/server"; import { redirect } from "next/navigation"; export default async function InvoicesPage() { const { orgId, has, isAuthenticated } = await auth(); if (!isAuthenticated) redirect("/sign-in"); if (!orgId) redirect("/org-selection"); const canRead = has({ permission: "org:invoices:read" }); const canCreate = has({ permission: "org:invoices:create" }); const canDelete = has({ permission: "org:invoices:delete" }); if (!canRead) redirect("/dashboard"); const invoices = await db.query.invoices.findMany({ where: eq(invoices.orgId, orgId), }); return ( <main> <h1>Invoices</h1> {canCreate && ( <a href="/invoices/new" className="btn btn-primary"> Create Invoice </a> )} <table> <thead> <tr> <th>Invoice #</th> <th>Amount</th> <th>Status</th> {canDelete && <th>Actions</th>} </tr> </thead> <tbody> {invoices.map((invoice) => ( <tr key={invoice.id}> <td>{invoice.number}</td> <td>${invoice.amount.toFixed(2)}</td> <td>{invoice.status}</td> {canDelete && ( <td> <form action={deleteInvoice}> <input type="hidden" name="id" value={invoice.id} /> <button type="submit" className="btn btn-danger"> Delete </button> </form> </td> )} </tr> ))} </tbody> </table> </main> ); } ``` **Why good:** Granular permission checks (read/create/delete), UI adapts based on permissions, page-level read check prevents unauthorized access, action column only shown to users with delete permission ### Good Example -- Permission-Protected Server Action ```ts // app/actions/delete-invoice.ts "use server"; import { auth } from "@clerk/nextjs/server"; export async function deleteInvoice(formData: FormData) { const { userId, orgId, has } = await auth(); if (!userId || !orgId) { throw new Error("Unauthorized"); } if (!has({ permission: "org:invoices:delete" })) { throw new Error("Insufficient permissions"); } const invoiceId = formData.get("id") as string; // Verify invoice belongs to this organization const invoice = await db.query.invoices.findFirst({ where: and(eq(invoices.id, invoiceId), eq(invoices.orgId, orgId)), }); if (!invoice) { throw new Error("Invoice not found"); } await db.delete(invoices).where(eq(invoices.id, invoiceId)); return { success: true }; } ``` **Why good:** Permission check in server action (defense in depth), verifies invoice belongs to active org (prevents cross-tenant access), `orgId` used for scoping --- ## Pattern 4: Client-Side Organization Management ### Good Example -- useOrganization Hook ```tsx // components/org-member-list.tsx "use client"; import { useOrganization } from "@clerk/nextjs"; export function OrgMemberList() { const { isLoaded, organization, membership, memberships } = useOrganization({ memberships: { pageSize: 20, keepPreviousData: true, }, }); if (!isLoaded) return <div>Loading...</div>; if (!organization) { return <p>No active organization</p>; } const isAdmin = membership?.role === "org:admin"; return ( <div className="member-list"> <h2>{organization.name} Members</h2> <p>{organization.membersCount} total members</p> <ul> {memberships?.data?.map((member) => ( <li key={member.id}> <span>{member.publicUserData.identifier}</span> <span className="role-badge">{member.role}</span> {isAdmin && member.role !== "org:admin" && ( <button onClick={async () => { await member.update({ role: "org:admin" }); }} > Promote to Admin </button> )} </li> ))} </ul> {memberships?.hasNextPage && ( <button onClick={() => memberships.fetchNext()}>Load More</button> )} </div> ); } ``` **Why good:** `useOrganization` with memberships pagination, role check before showing promote button, `fetchNext()` for pagination, `publicUserData.identifier` is the member's email ### Good Example -- Create Organization ```tsx // components/create-org-form.tsx "use client"; import { useOrganizationList } from "@clerk/nextjs"; import { useState, type FormEvent } from "react"; export function CreateOrgForm() { const { isLoaded, createOrganization, setActive } = useOrganizationList(); const [name, setName] = useState(""); const [creating, setCreating] = useState(false); if (!isLoaded) return null; async function handleSubmit(e: FormEvent) { e.preventDefault(); if (!name.trim()) return; setCreating(true); try { const org = await createOrganization({ name }); // Set the new org as active await setActive({ organization: org.id }); // Redirect or update UI window.location.assign("/dashboard"); } catch (err) { console.error("Failed to create organization:", err); } finally { setCreating(false); } } return ( <form onSubmit={handleSubmit}> <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Organization name" required /> <button type="submit" disabled={creating}> {creating ? "Creating..." : "Create Organization"} </button> </form> ); } ``` **Why good:** `createOrganization` from `useOrganizationList`, `setActive` switches to new org immediately, loading state during creation, error handling --- ## Pattern 5: Organization-Scoped Data Patterns ### Good Example -- Database Schema with Organization Scoping ```sql -- Database schema for Clerk-synced tables (use your ORM of choice) -- Users table: synced via Clerk webhooks CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), clerk_id TEXT NOT NULL UNIQUE, -- Clerk user ID email TEXT NOT NULL, first_name TEXT, last_name TEXT, image_url TEXT, created_at TIMESTAMP DEFAULT NOW() ); -- Organizations table: synced via Clerk webhooks CREATE TABLE organizations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), clerk_org_id TEXT NOT NULL UNIQUE, -- Clerk organization ID name TEXT NOT NULL, slug TEXT NOT NULL, image_url TEXT, created_at TIMESTAMP DEFAULT NOW() ); -- Organization members: synced via Clerk webhooks CREATE TABLE org_members ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id TEXT NOT NULL, -- Clerk organization ID user_id TEXT NOT NULL, -- Clerk user ID role TEXT NOT NULL, -- e.g., "org:admin", "org:member" created_at TIMESTAMP DEFAULT NOW() ); -- Application data: scoped to organization CREATE TABLE projects ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id TEXT NOT NULL, -- Clerk organization ID (foreign key for tenant scoping) name TEXT NOT NULL, created_by TEXT NOT NULL, -- Clerk user ID created_at TIMESTAMP DEFAULT NOW() ); ``` **Why good:** `clerkId`/`clerkOrgId` as foreign keys (synced via webhooks), all app data scoped to `orgId`, `createdBy` tracks user who created resource ### Good Example -- Organization-Scoped Data Access Layer ```ts // lib/data-access.ts import { auth } from "@clerk/nextjs/server"; // Always verify auth at the data access layer export async function getProjects() { const { userId, orgId } = await auth(); if (!userId || !orgId) { throw new Error( "Unauthorized: requires authenticated user with active org", ); } return db.query.projects.findMany({ where: eq(projects.orgId, orgId), orderBy: desc(projects.createdAt), }); } export async function getProject(projectId: string) { const { userId, orgId } = await auth(); if (!userId || !orgId) { throw new Error("Unauthorized"); } const project = await db.query.projects.findFirst({ where: and( eq(projects.id, projectId), eq(projects.orgId, orgId), // Prevents cross-tenant access ), }); if (!project) { throw new Error("Project not found"); } return project; } export async function createProject(name: string) { const { userId, orgId, has } = await auth(); if (!userId || !orgId) { throw new Error("Unauthorized"); } if (!has({ permission: "org:projects:create" })) { throw new Error("Insufficient permissions"); } return db .insert(projects) .values({ name, orgId, createdBy: userId, }) .returning(); } ``` **Why good:** Auth verified at data layer (defense in depth), every query scoped to `orgId` (prevents cross-tenant data leaks), permission checks for mutations, throws on unauthorized ### Bad Example -- No Organization Scoping ```ts // BAD: No org scoping -- leaks data across tenants export async function getProjects() { return db.query.projects.findMany(); // Returns ALL projects! } // BAD: No ownership check export async function deleteProject(id: string) { await db.delete(projects).where(eq(projects.id, id)); // Any user can delete } ``` **Why bad:** No org scoping means users see data from all organizations, no ownership check means any authenticated user can delete any project, cross-tenant data leak --- ## Pattern 6: Organization Profile Management ### Good Example -- Organization Settings Page ```tsx // app/org/settings/[[...settings]]/page.tsx import { OrganizationProfile } from "@clerk/nextjs"; import { auth } from "@clerk/nextjs/server"; import { redirect } from "next/navigation"; export default async function OrgSettingsPage() { const { orgId, has } = await auth(); if (!orgId) redirect("/org-selection"); // Only admins can access org settings if (!has({ role: "org:admin" })) { redirect("/dashboard"); } return ( <main> <h1>Organization Settings</h1> <OrganizationProfile /> </main> ); } ``` **Why good:** Catch-all route for multi-tab navigation, admin-only access check, `<OrganizationProfile>` provides member management, settings, and domain verification UI --- _For core setup, see [core.md](core.md). For webhook handling, see [server.md](server.md)._ -
server.md 12.7 KB
# Clerk Server-Side Auth Examples > Server Components, API route protection, Server Actions, and webhook handling. See [SKILL.md](../SKILL.md) for core concepts. **Core setup:** See [core.md](core.md). **Components:** See [components.md](components.md). **Client hooks:** See [hooks.md](hooks.md). --- ## Pattern 1: auth() in Server Components `auth()` is lightweight -- it reads session claims from the request cookie without making an API call. Safe to call multiple times (deduplicated per request). ### Good Example -- Protected Page ```tsx // app/dashboard/page.tsx import { auth } from "@clerk/nextjs/server"; export default async function DashboardPage() { const { userId, orgId, isAuthenticated, redirectToSignIn } = await auth(); if (!isAuthenticated) { return redirectToSignIn(); } // userId is guaranteed non-null after isAuthenticated check const dashboardData = await getDashboardData(userId, orgId); return ( <main> <h1>Dashboard</h1> <p>User: {userId}</p> {orgId && <p>Organization: {orgId}</p>} <DashboardContent data={dashboardData} /> </main> ); } ``` **Why good:** `auth()` is lightweight (no API call), `redirectToSignIn()` handles redirect, `orgId` may be null (user without active org), data fetched with user context ### Good Example -- Authorization with protect() ```tsx // app/admin/page.tsx import { auth } from "@clerk/nextjs/server"; export default async function AdminPage() { // Redirects to sign-in if unauthenticated, returns 404 if unauthorized await auth.protect({ role: "org:admin" }); return ( <main> <h1>Admin Panel</h1> <p>Only org admins can see this page.</p> </main> ); } ``` **Why good:** `protect()` handles both authentication and authorization in one call, unauthenticated users redirected, unauthorized users get 404 ### Good Example -- Permission-Based Check with has() ```tsx // app/billing/page.tsx import { auth } from "@clerk/nextjs/server"; import { redirect } from "next/navigation"; export default async function BillingPage() { const { has, isAuthenticated, redirectToSignIn } = await auth(); if (!isAuthenticated) { return redirectToSignIn(); } if (!has({ permission: "org:billing:manage" })) { redirect("/dashboard"); } return ( <main> <h1>Billing Management</h1> {/* billing content */} </main> ); } ``` **Why good:** `has()` for granular permission checks, custom redirect for unauthorized (instead of 404), separate auth and authorization checks for different handling --- ## Pattern 2: currentUser() in Server Components `currentUser()` makes a Backend API call to fetch the full user object. Use when you need user profile data server-side. ### Good Example -- Server-Side User Data ```tsx // 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"); } // IMPORTANT: Only pass specific fields to client components // currentUser() returns privateMetadata which must stay server-side const safeProfile = { firstName: user.firstName, lastName: user.lastName, imageUrl: user.imageUrl, email: user.emailAddresses.find((e) => e.id === user.primaryEmailAddressId) ?.emailAddress, createdAt: user.createdAt, }; return ( <main> <h1> {safeProfile.firstName} {safeProfile.lastName} </h1> <img src={safeProfile.imageUrl} alt="Profile" /> <p>{safeProfile.email}</p> <p> Member since: {new Date(safeProfile.createdAt).toLocaleDateString()} </p> {/* Safe to pass filtered data to client components */} <ProfileActions user={safeProfile} /> </main> ); } ``` **Why good:** Explicitly picks safe fields before passing to client, never passes full user object (contains `privateMetadata`), redirect for unauthenticated users ### Bad Example -- Passing Full User to Client ```tsx // BAD: Leaking privateMetadata to client import { currentUser } from "@clerk/nextjs/server"; export default async function Page() { const user = await currentUser(); // user.privateMetadata is exposed to the client! return <ClientComponent user={user} />; } ``` **Why bad:** `currentUser()` includes `privateMetadata` (sensitive server-only data), passing full object to client component leaks it to the browser, security vulnerability --- ## Pattern 3: Route Handlers (API Routes) ### Good Example -- Protected API Route ```ts // app/api/user/profile/route.ts import { auth } from "@clerk/nextjs/server"; import { NextResponse } from "next/server"; export async function GET() { const { userId } = await auth(); if (!userId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const profile = await db.query.users.findFirst({ where: eq(users.clerkId, userId), }); if (!profile) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } return NextResponse.json(profile); } export async function PATCH(req: Request) { const { userId } = await auth(); if (!userId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const body = await req.json(); const updated = await db .update(users) .set({ name: body.name, bio: body.bio }) .where(eq(users.clerkId, userId)) .returning(); return NextResponse.json(updated[0]); } ``` **Why good:** Auth check in every handler (defense in depth, not just middleware), `userId` scopes all queries to current user, proper HTTP status codes, both GET and PATCH protected ### Good Example -- Organization-Scoped API Route ```ts // app/api/org/members/route.ts import { auth } from "@clerk/nextjs/server"; import { NextResponse } from "next/server"; export async function GET() { const { userId, orgId, has } = await auth(); if (!userId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } if (!orgId) { return NextResponse.json( { error: "No active organization" }, { status: 400 }, ); } // Check permission for member management if (!has({ permission: "org:members:read" })) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } const members = await db.query.orgMembers.findMany({ where: eq(orgMembers.orgId, orgId), }); return NextResponse.json(members); } ``` **Why good:** Checks auth, active org, AND permission separately, proper error status codes (401/400/403), `orgId` scopes query to active organization --- ## Pattern 4: Server Actions ### Good Example -- Protected Server Action ```ts // app/actions/update-profile.ts "use server"; import { auth, currentUser } from "@clerk/nextjs/server"; export async function updateProfile(formData: FormData) { const { userId } = await auth(); if (!userId) { throw new Error("Unauthorized"); } const name = formData.get("name") as string; const bio = formData.get("bio") as string; // Update in your database await db.update(users).set({ name, bio }).where(eq(users.clerkId, userId)); // Optionally update Clerk user metadata const user = await currentUser(); if (user) { await user.update({ publicMetadata: { bio }, }); } return { success: true }; } ``` **Why good:** Auth check inside server action (not relying on middleware alone), `userId` scopes mutation, updates both local DB and Clerk metadata, `"use server"` directive ### Good Example -- Organization-Scoped Server Action ```ts // app/actions/create-project.ts "use server"; import { auth } from "@clerk/nextjs/server"; export async function createProject(formData: FormData) { const { userId, orgId, has } = await auth(); if (!userId) { throw new Error("Unauthorized"); } if (!orgId) { throw new Error("No active organization"); } if (!has({ permission: "org:projects:create" })) { throw new Error("Insufficient permissions"); } const name = formData.get("name") as string; const project = await db.insert(projects).values({ name, orgId, createdBy: userId, }); return { success: true, projectId: project.id }; } ``` **Why good:** Checks auth, org, and permission in server action, `orgId` scopes created resource to organization, `createdBy` tracks who created it --- ## Pattern 5: Webhook Handling ### Good Example -- Complete Webhook Handler ```ts // app/api/webhooks/clerk/route.ts import { verifyWebhook } from "@clerk/nextjs/webhooks"; import type { UserJSON, OrganizationJSON } from "@clerk/shared/types"; export async function POST(req: Request) { let evt; try { evt = await verifyWebhook(req); } catch { return new Response("Webhook verification failed", { status: 400 }); } switch (evt.type) { case "user.created": { const userData = evt.data as UserJSON; const primaryEmail = userData.email_addresses.find( (e) => e.id === userData.primary_email_address_id, )?.email_address; await db.insert(users).values({ clerkId: userData.id, email: primaryEmail ?? "", firstName: userData.first_name, lastName: userData.last_name, imageUrl: userData.image_url, }); break; } case "user.updated": { const userData = evt.data as UserJSON; const primaryEmail = userData.email_addresses.find( (e) => e.id === userData.primary_email_address_id, )?.email_address; await db .update(users) .set({ email: primaryEmail ?? "", firstName: userData.first_name, lastName: userData.last_name, imageUrl: userData.image_url, }) .where(eq(users.clerkId, userData.id)); break; } case "user.deleted": { const { id } = evt.data; if (id) { await db.delete(users).where(eq(users.clerkId, id)); } break; } case "organization.created": { const orgData = evt.data as OrganizationJSON; await db.insert(organizations).values({ clerkOrgId: orgData.id, name: orgData.name, slug: orgData.slug, imageUrl: orgData.image_url, }); break; } case "organizationMembership.created": { const memberData = evt.data; await db.insert(orgMembers).values({ orgId: memberData.organization.id, userId: memberData.public_user_data.user_id, role: memberData.role, }); break; } case "organizationMembership.deleted": { const memberData = evt.data; await db .delete(orgMembers) .where( and( eq(orgMembers.orgId, memberData.organization.id), eq(orgMembers.userId, memberData.public_user_data.user_id), ), ); break; } default: // Unhandled event type -- return 200 to acknowledge receipt break; } return new Response("OK", { status: 200 }); } ``` **Why good:** `verifyWebhook` validates Svix signature, handles multiple event types, try/catch for verification failure, always returns 200 (Clerk retries non-2xx), separate handlers for users and organizations ### Bad Example -- Unverified Webhook ```ts // BAD: No signature verification export async function POST(req: Request) { const body = await req.json(); // Anyone can send fake events to this endpoint! await db.insert(users).values({ clerkId: body.data.id, email: body.data.email_addresses[0].email_address, }); return new Response("OK"); } ``` **Why bad:** No signature verification, anyone can POST fake data, direct database insertion from untrusted input, major security vulnerability --- ## Pattern 6: Custom JWT Templates ### Good Example -- Fetching Custom Token for External API ```tsx // components/external-api-caller.tsx "use client"; import { useAuth } from "@clerk/nextjs"; const EXTERNAL_API_URL = "https://api.example.com/data"; export function ExternalApiCaller() { const { getToken, isSignedIn } = useAuth(); async function callExternalApi() { if (!isSignedIn) return; try { // Fetch a JWT using a custom template configured in Clerk Dashboard const token = await getToken({ template: "my-external-api" }); const response = await fetch(EXTERNAL_API_URL, { headers: { Authorization: `Bearer ${token}` }, }); return response.json(); } catch (err) { // Core 3: throws ClerkOfflineError when offline console.error("Failed to get token:", err); } } return ( <button onClick={callExternalApi} disabled={!isSignedIn}> Fetch External Data </button> ); } ``` **Why good:** Custom JWT template for external services, named constant for URL, try/catch for offline handling, configured in Clerk Dashboard under JWT Templates --- _For organizations and multi-tenancy, see [organizations.md](organizations.md)._
-
-
reference.md 9.7 KB
# Clerk Quick Reference > Environment variables, Clerk Dashboard setup, webhook events, and common configuration. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) for code examples. --- ## Environment Variables | Variable | Required | Description | | ------------------------------------------------- | ------------ | --------------------------------------------------- | | `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Yes | Public key from Clerk Dashboard (starts with `pk_`) | | `CLERK_SECRET_KEY` | Yes | Secret key from Clerk Dashboard (starts with `sk_`) | | `NEXT_PUBLIC_CLERK_SIGN_IN_URL` | No | Custom sign-in page path (default: Clerk hosted) | | `NEXT_PUBLIC_CLERK_SIGN_UP_URL` | No | Custom sign-up page path (default: Clerk hosted) | | `NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL` | No | Redirect after sign-in (default: `/`) | | `NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL` | No | Redirect after sign-up (default: `/`) | | `CLERK_WEBHOOK_SIGNING_SECRET` | For webhooks | Webhook signing secret (starts with `whsec_`) | **Keys are found in:** Clerk Dashboard > Configure > API Keys **Webhook secret is found in:** Clerk Dashboard > Webhooks > Select endpoint > Signing Secret --- ## Clerk Dashboard Setup Checklist 1. Create application at [dashboard.clerk.com](https://dashboard.clerk.com) 2. Configure authentication methods (email, social, phone) 3. Copy API keys to `.env.local` 4. Enable Organizations (if needed): Configure > Organizations 5. Set up roles and permissions (if needed): Configure > Organizations > Roles 6. Add webhook endpoint (if needed): Webhooks > Add Endpoint 7. Configure redirect URLs: Configure > Paths --- ## Webhook Event Types ### User Events | Event | Trigger | Payload | | -------------- | -------------------- | ---------------- | | `user.created` | New user signs up | `UserJSON` | | `user.updated` | User profile changes | `UserJSON` | | `user.deleted` | User account deleted | `{ id: string }` | ### Session Events | Event | Trigger | Payload | | ----------------- | --------------- | ------------- | | `session.created` | User signs in | `SessionJSON` | | `session.ended` | User signs out | `SessionJSON` | | `session.removed` | Session revoked | `SessionJSON` | ### Organization Events | Event | Trigger | Payload | | --------------------------------- | -------------------- | ---------------------------- | | `organization.created` | New org created | `OrganizationJSON` | | `organization.updated` | Org settings changed | `OrganizationJSON` | | `organization.deleted` | Org deleted | `{ id: string }` | | `organizationMembership.created` | Member added | `OrganizationMembershipJSON` | | `organizationMembership.updated` | Member role changed | `OrganizationMembershipJSON` | | `organizationMembership.deleted` | Member removed | `OrganizationMembershipJSON` | | `organizationInvitation.created` | Invite sent | `OrganizationInvitationJSON` | | `organizationInvitation.accepted` | Invite accepted | `OrganizationInvitationJSON` | | `organizationInvitation.revoked` | Invite revoked | `OrganizationInvitationJSON` | ### Email & SMS Events | Event | Trigger | Payload | | --------------- | -------------------- | ----------- | | `email.created` | Email sent via Clerk | `EmailJSON` | | `sms.created` | SMS sent via Clerk | `SMSJSON` | --- ## Default Organization Roles | Role | Slug | Default Permissions | | ------ | ------------ | ------------------------------------------------------------------------- | | Admin | `org:admin` | All system permissions (manage members, manage org, manage billing, etc.) | | Member | `org:member` | Read members, read billing | **Custom permissions format:** `org:<feature>:<action>` (e.g., `org:invoices:create`, `org:reports:read`) --- ## Hooks Quick Reference | Hook | Purpose | Key Returns | | ----------------------- | --------------------- | ------------------------------------------------------------------ | | `useUser()` | User profile data | `isLoaded`, `isSignedIn`, `user` | | `useAuth()` | Auth state and tokens | `userId`, `sessionId`, `orgId`, `getToken()`, `has()`, `signOut()` | | `useClerk()` | Low-level Clerk API | Full Clerk instance | | `useSession()` | Current session info | `isLoaded`, `isSignedIn`, `session` | | `useOrganization()` | Active org data | `organization`, `membership`, `isLoaded` | | `useOrganizationList()` | All user orgs | `organizationList`, `isLoaded`, `createOrganization()` | | `useSignIn()` | Custom sign-in flows | `signIn`, `setActive`, `isLoaded` | | `useSignUp()` | Custom sign-up flows | `signUp`, `setActive`, `isLoaded` | --- ## Server-Side Helpers Quick Reference | Helper | Import | Context | Returns | | ---------------------- | ------------------------ | ------------------------------------------------- | -------------------------------------------------------------------------- | | `auth()` | `@clerk/nextjs/server` | Server Components, Route Handlers, Server Actions | `userId`, `sessionId`, `orgId`, `protect()`, `has()`, `redirectToSignIn()` | | `currentUser()` | `@clerk/nextjs/server` | Server Components, Route Handlers, Server Actions | Full `BackendUser` object or `null` | | `clerkMiddleware()` | `@clerk/nextjs/server` | Middleware file | Middleware handler | | `createRouteMatcher()` | `@clerk/nextjs/server` | Middleware file | Route matching function | | `verifyWebhook()` | `@clerk/nextjs/webhooks` | Webhook Route Handlers | Verified webhook event | --- ## Component Quick Reference | Component | Purpose | Usage | | -------------------------- | ------------------------- | ----------------------------------- | | `<ClerkProvider>` | Auth context wrapper | Wrap app in layout.tsx | | `<SignIn />` | Full sign-in form | Dedicated page with catch-all route | | `<SignUp />` | Full sign-up form | Dedicated page with catch-all route | | `<UserButton />` | Avatar with dropdown menu | Header/nav bar | | `<UserProfile />` | Full profile management | Dedicated settings page | | `<OrganizationSwitcher />` | Org picker + creator | Header/sidebar | | `<OrganizationProfile />` | Org settings management | Org settings page | | `<OrganizationList />` | List user's organizations | Org selection page | | `<Show>` | Conditional rendering | Auth/role/permission gating | | `<SignInButton />` | Button that opens sign-in | Header when signed out | | `<SignUpButton />` | Button that opens sign-up | Header when signed out | --- ## Middleware File Naming | Next.js Version | Filename | Location | | --------------- | --------------- | ---------------------- | | Next.js 16+ | `proxy.ts` | Project root or `src/` | | Next.js <=15 | `middleware.ts` | Project root or `src/` | The code is identical in both cases -- only the filename differs. --- ## Core 3 Migration Cheat Sheet | Core 2 (Deprecated) | Core 3 (Current) | | --------------------------------- | --------------------------------------- | | `<SignedIn>` | `<Show when="signed-in">` | | `<SignedOut>` | `<Show when="signed-out">` | | `<Protect role="admin">` | `<Show when={{ role: "org:admin" }}>` | | `<Protect permission="...">` | `<Show when={{ permission: "..." }}>` | | `<Protect condition={...}>` | `<Show when={(has) => has(...)}>` | | `@clerk/clerk-react` | `@clerk/react` | | `@clerk/types` | `@clerk/shared/types` | | `appearance.layout` | `appearance.options` | | `authMiddleware()` | `clerkMiddleware()` | | `middleware.ts` (Next.js 16) | `proxy.ts` | | `getToken()` returns null offline | `getToken()` throws `ClerkOfflineError` | --- ## Version Requirements (Core 3) - Node.js 20.9.0+ - Next.js 15.2.3+ - Expo SDK 53+ Run `npx @clerk/upgrade` to automate migration. -
SKILL.md 16.2 KB
--- name: api-auth-clerk description: Clerk managed authentication - ClerkProvider, middleware, pre-built components, hooks, server-side auth, organizations, webhooks --- # Clerk Authentication Patterns > **Quick Guide:** Clerk provides managed authentication with pre-built UI components, server-side helpers, and organization-based multi-tenancy. Use `clerkMiddleware()` for route protection, `<Show>` for conditional rendering, hooks for client state, and `auth()`/`currentUser()` for server-side auth. Clerk Core 3 (2026) replaces `<SignedIn>`/`<SignedOut>` with `<Show>`, renames the middleware file to `proxy.ts` (Next.js 16+), and consolidates packages. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use `@clerk/nextjs/server` for ALL server-side imports -- NEVER import server helpers from `@clerk/nextjs`)** **(You MUST verify webhooks using Clerk's `verifyWebhook` helper -- NEVER trust unverified webhook payloads)** **(You MUST use `<Show>` component instead of deprecated `<SignedIn>`/`<SignedOut>`/`<Protect>` -- these are removed in Core 3)** **(You MUST NOT pass the full `currentUser()` object to the client -- it contains `privateMetadata` that must stay server-side)** **(You MUST protect routes in BOTH middleware AND data access layer -- middleware alone is insufficient)** </critical_requirements> --- **Auto-detection:** Clerk, ClerkProvider, clerkMiddleware, @clerk/nextjs, useUser, useAuth, useClerk, useSession, useOrganization, SignIn, SignUp, UserButton, UserProfile, OrganizationSwitcher, auth(), currentUser(), CLERK_SECRET_KEY, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, Show when="signed-in" **When to use:** - Adding authentication and user management to an application - Building multi-tenant B2B apps with organization-based access control - Using pre-built sign-in/sign-up UI components with customizable theming - Protecting routes with middleware and server-side authorization checks - Syncing Clerk user data to your database via webhooks **Key patterns covered:** - ClerkProvider setup, environment variables, middleware configuration - Pre-built UI components (`<SignIn>`, `<SignUp>`, `<UserButton>`, `<Show>`) - Client-side hooks (`useUser`, `useAuth`, `useSession`, `useOrganization`) - Server-side auth (`auth()`, `currentUser()`) in Server Components, Route Handlers, Server Actions - Middleware route protection with `clerkMiddleware()` and `createRouteMatcher()` - Organization-based multi-tenancy with roles and permissions - Webhook handling with Svix signature verification **When NOT to use:** - Self-hosted auth requirement (need full control over auth data storage) - Cannot use a third-party auth service (compliance/regulatory constraints) - Simple API key authentication (custom middleware is sufficient) - Budget constraints prevent using a managed service **Detailed Resources:** - [reference.md](reference.md) - Decision frameworks, hooks quick reference, Core 3 migration cheat sheet - [examples/core.md](examples/core.md) - ClerkProvider, environment variables, middleware configuration - [examples/components.md](examples/components.md) - Pre-built components, customization, appearance prop - [examples/hooks.md](examples/hooks.md) - useUser, useAuth, useSession, loading states, conditional rendering - [examples/server.md](examples/server.md) - Server Components, API routes, Server Actions, webhook handling - [examples/organizations.md](examples/organizations.md) - Organization management, roles, permissions, RBAC --- <philosophy> ## Philosophy Clerk is a **managed authentication platform** that handles the entire auth lifecycle: sign-up, sign-in, session management, user profiles, organizations, and MFA. Instead of building auth from scratch, you integrate Clerk's SDK and pre-built components. **Core principles:** 1. **Defense in depth** -- Protect routes at the middleware layer AND verify auth at every data access point. Middleware alone is insufficient (CVE-2025-29927 demonstrated middleware bypass vulnerabilities). 2. **Server-first auth** -- Use `auth()` and `currentUser()` in Server Components and Route Handlers. Only use client hooks (`useUser`, `useAuth`) when you need reactive client-side state. 3. **Pre-built over custom** -- Use Clerk's `<SignIn>`, `<SignUp>`, `<UserButton>` components. Only build custom flows when the pre-built components genuinely cannot meet requirements. 4. **Organizations for multi-tenancy** -- Use Clerk Organizations with roles and permissions for B2B apps. Do not build custom tenant systems on top of Clerk's user model. 5. **Webhook-driven sync** -- Sync Clerk data to your database via webhooks, not by polling. Always verify webhook signatures with `verifyWebhook`. **When to use Clerk:** - You need auth quickly with minimal custom code - You want pre-built UI components for sign-in/sign-up/user management - You need organization-based multi-tenancy with RBAC - You want managed MFA, SSO (SAML/OIDC), and social login **When NOT to use Clerk:** - You need full control over auth data storage (self-hosted requirement) - You cannot use a third-party auth service (compliance/regulatory) - Your app only needs simple API key authentication - Budget constraints prevent using a managed service </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: ClerkProvider and Middleware Setup Every Clerk app needs `<ClerkProvider>` wrapping the app and `clerkMiddleware()` protecting routes. See [examples/core.md](examples/core.md) for full setup examples. **Key rules:** - `ClerkProvider` goes inside `<body>`, not wrapping `<html>` -- Core 3 requires this - Use `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` env var, never hardcode keys - Middleware file is `proxy.ts` on Next.js 16+ or `middleware.ts` on Next.js <=15 - Webhook endpoint must be in the public routes list (verified separately by `verifyWebhook`) - `CLERK_WEBHOOK_SIGNING_SECRET` is the env var for webhook signing (not `CLERK_WEBHOOK_SECRET`) ```ts // proxy.ts (Next.js 16+) or middleware.ts (Next.js <=15) const isPublicRoute = createRouteMatcher([ "/", "/sign-in(.*)", "/sign-up(.*)", "/api/webhooks(.*)", ]); export default clerkMiddleware(async (auth, req) => { if (!isPublicRoute(req)) { await auth.protect(); } }); ``` **Why good:** Public routes explicitly whitelisted, everything else requires auth, webhook endpoint is public (verified separately) ```ts // BAD: clerkMiddleware() with no callback export default clerkMiddleware(); // All routes are PUBLIC by default! ``` **Why bad:** `clerkMiddleware()` without callback attaches auth data but does not protect any route --- ### Pattern 2: Pre-Built UI Components and `<Show>` Use `<Show>` for conditional rendering (Core 3 replacement for `<SignedIn>`/`<SignedOut>`/`<Protect>`). See [examples/components.md](examples/components.md) for full examples. ```tsx <Show when="signed-out"><SignInButton /></Show> <Show when="signed-in"><UserButton /></Show> <Show when={{ role: "org:admin" }}><AdminPanel /></Show> <Show when={(has) => has({ permission: "org:invoices:manage" })}><InvoiceManager /></Show> ``` **Why good:** `<Show>` is the Core 3 API, supports string/object/callback conditions, `fallback` prop for unauthorized ```tsx // BAD: Deprecated -- removed in Core 3 <SignedIn>...</SignedIn> <SignedOut>...</SignedOut> <Protect role="admin">...</Protect> ``` **Why bad:** `<SignedIn>`, `<SignedOut>`, `<Protect>` removed in Core 3, use `<Show>` with `when` prop Sign-in/sign-up pages require catch-all route segments `[[...sign-in]]` for multi-step flows (MFA, OAuth callbacks). --- ### Pattern 3: Client-Side Hooks Use hooks in Client Components for reactive auth state. **Always check `isLoaded` before accessing data.** See [examples/hooks.md](examples/hooks.md) for complete examples. ```tsx "use client"; const { isLoaded, isSignedIn, user } = useUser(); if (!isLoaded) return <div>Loading...</div>; if (!isSignedIn) return <div>Please sign in</div>; // Now safe to access user.firstName, user.emailAddresses, etc. ``` **Why good:** Prevents hydration errors from accessing `undefined` during init, prevents TypeError from accessing `null` when signed out ```tsx // BAD: Accessing user without guards const { user } = useUser(); return <p>{user.firstName}</p>; // Runtime error when isLoaded=false or isSignedIn=false ``` **Why bad:** `user` is `undefined` until loaded and `null` when signed out **Key hooks:** - `useUser()` -- user profile data (name, email, avatar) - `useAuth()` -- session tokens (`getToken()`), `userId`, `orgId`, `has()` for authorization - `useOrganization()` -- active org data, membership, paginated members list - `useReverification()` -- re-authenticate before sensitive actions (Core 3) In Core 3, `getToken()` throws `ClerkOfflineError` when offline instead of returning null -- always wrap in try/catch. --- ### Pattern 4: Server-Side Authentication Use `auth()` and `currentUser()` from `@clerk/nextjs/server`. See [examples/server.md](examples/server.md) for complete examples. **`auth()` -- lightweight, no API call, reads session claims:** ```tsx import { auth } from "@clerk/nextjs/server"; const { userId, orgId, isAuthenticated, redirectToSignIn } = await auth(); if (!isAuthenticated) return redirectToSignIn(); ``` **`currentUser()` -- full user object, makes Backend API call:** ```tsx import { currentUser } from "@clerk/nextjs/server"; const user = await currentUser(); // CRITICAL: Pick safe fields before passing to client components const safeData = { firstName: user.firstName, imageUrl: user.imageUrl }; ``` **Why critical:** `currentUser()` returns `privateMetadata` that must never reach the client. Explicitly pick fields. **Defense in depth:** Auth MUST be checked in every Server Action and Route Handler, not just middleware: ```ts "use server"; const { userId } = await auth(); if (!userId) throw new Error("Unauthorized"); // userId scopes all queries/mutations to current user ``` --- ### Pattern 5: Authorization with Roles and Permissions Use `auth.protect()` or `has()` for granular access control. See [examples/organizations.md](examples/organizations.md) for RBAC patterns. ```ts // Middleware: role-based route protection await auth.protect((has) => has({ role: "org:admin" })); // Server Component: permission check const { has } = await auth(); if (!has({ permission: "org:invoices:delete" })) redirect("/dashboard"); // Client: conditional rendering by role <Show when={{ role: "org:admin" }}><AdminPanel /></Show> ``` **Permission format:** `org:<feature>:<action>` (e.g., `org:invoices:create`, `org:reports:read`) **Default roles:** `org:admin` (all permissions), `org:member` (read-only). Custom roles configured in Clerk Dashboard. --- ### Pattern 6: Webhook Handling Use Clerk webhooks (via Svix) to sync user data to your database. See [examples/server.md](examples/server.md) for complete handler. ```ts import { verifyWebhook } from "@clerk/nextjs/webhooks"; export async function POST(req: Request) { const evt = await verifyWebhook(req); // reads CLERK_WEBHOOK_SIGNING_SECRET env var // evt.type: "user.created" | "user.updated" | "user.deleted" | "organization.*" | ... // evt.data: UserJSON | OrganizationJSON | etc. } ``` **Why good:** `verifyWebhook` validates Svix signature, always returns 200 to prevent retries, webhook route is public in middleware (verified by signature not by auth) ```ts // BAD: No signature verification const body = await req.json(); // Anyone can POST fake data! await db.insert(users).values(body.data); // Security vulnerability ``` **Why bad:** No signature verification means anyone can send fake webhook events to your endpoint **Import types from `@clerk/shared/types` (Core 3):** `UserJSON`, `OrganizationJSON`, `OrganizationMembershipJSON` </patterns> --- <decision_framework> ## Decision Framework ### Client vs Server Auth ``` Where do you need auth data? |-- Server Component, Route Handler, Server Action | |-- Need just userId/sessionId? --> auth() | |-- Need full user object? --> currentUser() | |-- Need to protect the route? --> auth.protect() | +-- Need org context? --> auth() returns orgId, orgRole | +-- Client Component (interactive UI) |-- Need user profile data? --> useUser() |-- Need session/token data? --> useAuth() |-- Need org data? --> useOrganization() +-- Need low-level Clerk API? --> useClerk() ``` ### Route Protection Strategy ``` What kind of route is it? |-- Public (landing, sign-in, sign-up, webhooks) | +-- Add to isPublicRoute matcher, skip auth.protect() | |-- Authenticated (dashboard, profile, settings) | +-- auth.protect() in middleware + auth() check in data layer | +-- Authorized (admin, org-specific, permission-gated) |-- Role-based? --> auth.protect({ role: "org:admin" }) +-- Permission-based? --> auth.protect((has) => has({ permission: "org:feature:action" })) ``` ### Component Choice ``` What auth UI do you need? |-- Full sign-in page --> <SignIn /> on catch-all route |-- Full sign-up page --> <SignUp /> on catch-all route |-- Sign-in button (modal) --> <SignInButton /> |-- User avatar + menu --> <UserButton /> |-- Full profile editor --> <UserProfile /> |-- Org switcher --> <OrganizationSwitcher /> +-- Conditional content --> <Show when="signed-in"> or <Show when={{ role: "..." }}> ``` </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Importing server helpers from `@clerk/nextjs` instead of `@clerk/nextjs/server` (breaks in Server Components) - Using deprecated `<SignedIn>`/`<SignedOut>`/`<Protect>` components (removed in Core 3) - Passing full `currentUser()` object to client components (leaks `privateMetadata`) - Trusting webhook payloads without `verifyWebhook` signature verification - Relying solely on middleware for route protection (middleware can be bypassed) - Importing types from `@clerk/types` instead of `@clerk/shared/types` (Core 3 rename) **Medium Priority Issues:** - Not checking `isLoaded` before accessing hook data (causes hydration errors) - Using `currentUser()` on the client side (it is server-only) - Hardcoding Clerk keys instead of using environment variables - Using `authMiddleware()` (deprecated, replaced by `clerkMiddleware()`) - Not making webhook endpoint public in middleware matcher - Using `CLERK_WEBHOOK_SECRET` instead of `CLERK_WEBHOOK_SIGNING_SECRET` **Common Mistakes:** - Naming middleware file `middleware.ts` on Next.js 16+ (should be `proxy.ts`) or `proxy.ts` on Next.js <=15 (should be `middleware.ts`) - Forgetting catch-all segments `[[...sign-in]]` on sign-in/sign-up pages (breaks multi-step flows) - Using `getToken()` without try/catch in Core 3 (throws `ClerkOfflineError` when offline instead of returning null) - Not adding `prefetch={false}` to `<Link>` components pointing at protected routes from public pages **Gotchas & Edge Cases:** - `currentUser()` counts against Backend API rate limits -- prefer `useUser()` hook on the client when possible - `auth()` in Server Components is deduplicated per request (safe to call multiple times) - `<Show when={{ role: "org:admin" }}>` requires an active organization in the session - Organization roles use the `org:` prefix (e.g., `org:admin`, `org:member`, `org:billing`) - Clerk Core 3 requires Node.js 20.9.0+, Next.js 15.2.3+ - `@clerk/clerk-react` renamed to `@clerk/react` in Core 3 -- update imports after upgrade </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use `@clerk/nextjs/server` for ALL server-side imports -- NEVER import server helpers from `@clerk/nextjs`)** **(You MUST verify webhooks using Clerk's `verifyWebhook` helper -- NEVER trust unverified webhook payloads)** **(You MUST use `<Show>` component instead of deprecated `<SignedIn>`/`<SignedOut>`/`<Protect>` -- these are removed in Core 3)** **(You MUST NOT pass the full `currentUser()` object to the client -- it contains `privateMetadata` that must stay server-side)** **(You MUST protect routes in BOTH middleware AND data access layer -- middleware alone is insufficient)** **Failure to follow these rules will create authentication vulnerabilities or break on Clerk Core 3.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.