clerk-nextjs-patterns
We need to translate the given English text into Turkish, preserving the name "clerk-nextjs-patterns" if it appears. The text is a description of an agent skill. We must not add any extra commentary, labels, or formatting. The translation should be accurate and natural in Turkish, keeping technical terms like "Next.js", "Clerk", "auth()", "useAuth()", "unstable_cache", "Server Actions", "API route", "HTTP status codes", "401", "403", "Core 2" as is. Also preserve "middleware", "authentication", "user-scoped caching", etc. but translate the surrounding words. The text: "Advanced Next.js patterns for authentication, middleware, Server Actions, and user-scoped caching with Clerk. Distinguishes server-side await auth() from client-side useAuth() hook; mixing them is a common breaking mistake Covers middleware strategies (public-first vs protected-first), API route protection, and proper HTTP status codes (401 vs 403) Includes user-scoped caching patterns with unstable_cache
npx skills add https://github.com/clerk/skills --skill clerk-nextjs-patternsNext.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
// 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:
import { Show } from '@clerk/nextjs'
<Show when="signed-in" fallback={<p>Please sign in</p>}>
<Dashboard />
</Show>
Authorization checks (B2B):
// 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:
<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):
import { 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):
'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:
'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):
import { 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):
import 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