Next.js Patterns
Version: Check
package.jsonfor the SDK version — seeclerkskill for the version table. Core 2 differences are noted inline with> **Core 2 ONLY (skip if current SDK):**callouts.
For basic setup, see clerk-setup skill.
What Do You Need?
| Task | Reference |
|---|---|
Server vs client auth (auth() vs hooks) | references/server-vs-client.md |
| Configure middleware (public-first vs protected-first) | references/middleware-strategies.md |
| Protect Server Actions | references/server-actions.md |
| API route auth (401 vs 403) | references/api-routes.md |
| Cache auth data (user-scoped caching) | references/caching-auth.md |
References
| Reference | Description |
|---|---|
references/server-vs-client.md | await auth() vs hooks |
references/middleware-strategies.md | Public-first vs protected-first, proxy.ts (Next.js <=15: middleware.ts) |
references/server-actions.md | Protect mutations |
references/api-routes.md | 401 vs 403 |
references/caching-auth.md | User-scoped caching |
Mental Model
Server vs Client = different auth APIs:
- Server:
await auth()from@clerk/nextjs/server(async!) - Client:
useAuth()hook from@clerk/nextjs(sync)
Never mix them. Server Components use server imports, Client Components use hooks.
Key properties from auth():
isAuthenticated— boolean, replaces the!!userIdpatternsessionStatus—'active'|'pending', for detecting incomplete session tasksuserId,orgId,orgSlug,has(),protect()— unchanged
Core 2 ONLY (skip if current SDK):
isAuthenticatedandsessionStatusare not available. Check!!userIdinstead.
Minimal Pattern
typescript// Server Component import { auth } from '@clerk/nextjs/server' export default async function Page() { const { isAuthenticated, userId } = await auth() // MUST await! if (!isAuthenticated) return <p>Not signed in</p> return <p>Hello {userId}</p> }
Core 2 ONLY (skip if current SDK):
isAuthenticatedis not available. Useif (!userId)instead.
Conditional Rendering with <Show>
For client-side conditional rendering based on auth state. <Show> covers both authentication checks and authorization (feature, plan, role, permission) in one component.
Authentication check:
tsximport { Show } from '@clerk/nextjs' <Show when="signed-in" fallback={<p>Please sign in</p>}> <Dashboard /> </Show>
Authorization checks (B2B):
tsx// Feature-based (preferred — features can move between plans without redeploy) <Show when={{ feature: 'analytics' }} fallback={<UpgradePrompt />}> <AnalyticsDashboard /> </Show> // Permission-based (preferred over role-based for granular access) <Show when={{ permission: 'org:invoices:create' }}> <NewInvoiceButton /> </Show> // Plan-based (tier-level gating) <Show when={{ plan: 'pro' }}> <ProFeatures /> </Show> // Role-based (use sparingly — prefer permission) <Show when={{ role: 'org:admin' }}> <AdminPanel /> </Show>
Callback for complex logic:
tsx<Show when={(has) => has({ role: 'org:admin' }) || has({ role: 'org:billing_manager' })}> <BillingActions /> </Show>
Core 2 ONLY (skip if current SDK):
<Show>does not exist. For authentication, use<SignedIn>and<SignedOut>. For authorization (role / permission), use<Protect>with the same prop names (role,permission,condition). Feature- and plan-based variants require Core 3. Seeclerk-custom-uiskill,core-3/show-component.mdfor the full migration table.
Common Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
undefined userId in Server Component | Missing await | await auth() not auth() |
| Auth not working on API routes | Missing matcher | Add `'/(api |
| Cache returns wrong user's data | Missing userId in key | Include userId in unstable_cache key |
| Mutations bypass auth | Unprotected Server Action | Check auth() at start of action |
| Wrong HTTP error code | Confused 401/403 | 401 = not signed in, 403 = no permission |
Session Tokens & Custom JWTs
getToken() for external APIs
Pass a custom JWT to third-party services (Hasura, Supabase, etc.) using JWT templates defined in the Clerk dashboard.
Server-side (Server Component or Route Handler):
typescriptimport { auth } from '@clerk/nextjs/server' export default async function Page() { const { getToken } = await auth() const token = await getToken({ template: 'hasura' }) if (!token) return <p>Not authenticated</p> const res = await fetch('https://api.example.com/graphql', { headers: { Authorization: `Bearer ${token}` }, }) const data = await res.json() return <pre>{JSON.stringify(data)}</pre> }
Client-side (Client Component):
typescript'use client' import { useAuth } from '@clerk/nextjs' export function DataFetcher() { const { getToken } = useAuth() async function fetchData() { const token = await getToken({ template: 'supabase' }) if (!token) return const res = await fetch('https://api.example.com/data', { headers: { Authorization: `Bearer ${token}` }, }) return res.json() } return <button onClick={fetchData}>Fetch</button> }
getToken() returns null when the user is not authenticated — always null-check before use.
useSession() for session data
Access session metadata in client components:
typescript'use client' import { useSession } from '@clerk/nextjs' export function SessionInfo() { const { session } = useSession() if (!session) return null return ( <p> Session {session.id} — last active: {session.lastActiveAt.toISOString()} </p> ) }
Manual JWT verification (no Clerk middleware)
For standalone API servers that receive Clerk session tokens from the Authorization header or the __session cookie (same-origin).
Using @clerk/backend verifyToken (recommended):
typescriptimport { verifyToken } from '@clerk/backend' const token = req.headers.authorization?.replace('Bearer ', '') if (!token) return res.status(401).json({ error: 'No token' }) try { const claims = await verifyToken(token, { jwtKey: process.env.CLERK_JWT_KEY, }) // claims.sub = userId } catch { return res.status(401).json({ error: 'Invalid token' }) }
Using jsonwebtoken (when you can't use @clerk/backend):
typescriptimport jwt from 'jsonwebtoken' const publicKey = process.env.CLERK_PEM_PUBLIC_KEY!.replace(/\\n/g, '\n') const token = req.headers.authorization?.replace('Bearer ', '') if (!token) return res.status(401).json({ error: 'No token' }) try { const claims = jwt.verify(token, publicKey, { algorithms: ['RS256'] }) as jwt.JwtPayload // Manually check exp and nbf (jsonwebtoken does this automatically, but verify azp if needed) // claims.sub = userId } catch { return res.status(401).json({ error: 'Invalid or expired token' }) }
Token sources:
- Same-origin requests:
__sessioncookie (Clerk sets this automatically) - Cross-origin / mobile / API-to-API:
Authorization: Bearer <token>header
CRITICAL: Always check
expandnbfclaims.verifyTokenfrom@clerk/backendhandles this automatically; with rawjsonwebtoken, setignoreExpiration: false(default) and ensureclockToleranceis minimal.
See Also
clerk-setup- Initial Clerk installclerk-orgs- B2B patterns (active org, role/permission gating)clerk-billing- Plan and feature entitlements withhas()clerk-webhooks- Sync user/org events to your databaseclerk-custom-ui- Theming and customization for built-in components


