Clerk Nextjs Patterns
ClerkOfficialCreated by clerk
- Skill
- Tool integration
- Software development
- MIT
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides architectural guidance and implementation patterns for securing Next.js applications with Clerk. It covers essential topics such as route protection, handling authentication in Server Components versus Client Components, managing API route security, and implementing user-scoped data caching. The guide emphasizes modern Next.js practices, including the use of Suspense boundaries for auth-dependent UI and proper handling of Server Actions to ensure secure mutations.
What it does
- Defines patterns for protecting pages, Route Handlers, and Server Actions.
- Explains the distinction between server-side auth() and client-side useAuth() hooks.
- Provides strategies for user-scoped caching using 'use cache' and Suspense.
- Offers guidance on conditional rendering with the Show component.
- Details manual JWT verification for standalone API servers.
- Addresses common build-time errors related to Next.js rendering modes.
When to use it
- When implementing authentication in a Next.js application using Clerk.
- When you need to secure Server Actions or API routes.
- When optimizing performance with user-specific data caching.
- When migrating or troubleshooting auth-related build errors in Next.js.
- When building B2B applications requiring role or permission-based access control.
Advantages
- Ensures consistent security practices across the entire application.
- Provides clear separation between server and client authentication logic.
- Helps prevent data leaks by enforcing user-scoped cache keys.
- Reduces build-time issues by aligning with Next.js rendering requirements.
- Simplifies complex authorization logic with reusable components.
Usage examples
- “How do I protect a Server Action in Next.js using Clerk?”
- The skill provides a pattern to call await auth() at the start of your Server Action to verify the user's identity before executing any mutations.
- “How can I cache user-specific data without leaking it to others?”
- The skill demonstrates how to read the userId outside of a 'use cache' function and pass it as an argument, ensuring the cache key is unique to that user.
- “Why is my Next.js build failing with an uncached data error?”
- The skill explains that reading auth data outside of a Suspense boundary causes prerendering failures, and provides instructions on wrapping auth-dependent UI in Suspense.
- “How do I verify a Clerk JWT on a standalone backend API?”
- The skill provides code examples using @clerk/backend's verifyToken or standard jsonwebtoken libraries to validate tokens from the Authorization header.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/clerk/clerk-nextjs-patterns/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip clerk-nextjs-patterns.zip -d .agents/skills
Documentation by clerk
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.
Route protection: call
await auth.protect()in each page, Route Handler, and Server Function. KeepclerkMiddleware()bare.createRouteMatcheris deprecated in@clerk/nextjs7 and will be removed in the next major version. Don't add it. If a project already uses it, leave it working and point the user to the migration guide.
What Do You Need?
| Task | Reference |
|---|---|
Server vs client auth (auth() vs hooks) | references/server-vs-client.md |
| Protect routes (check in each page, handler, and Server Function) | 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 |
Fix next build errors under Cache Components | Cache Components |
References
| Reference | Description |
|---|---|
references/server-vs-client.md | await auth() vs hooks |
references/middleware-strategies.md | auth.protect() in each resource, bare clerkMiddleware() in 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 with 'use cache' |
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'
import { Suspense } from 'react'
async function Greeting() {
const { isAuthenticated, userId } = await auth() // MUST await!
if (!isAuthenticated) return <p>Not signed in</p>
return <p>Hello {userId}</p>
}
export default function Page() {
return (
<Suspense>
<Greeting />
</Suspense>
)
}auth() reads the request, so it runs in an async child inside <Suspense>, not in the page itself. See Cache Components.
Core 2 ONLY (skip if current SDK):
isAuthenticatedis not available. Useif (!userId)instead.
Conditional Rendering with <Show>
<Show> renders its children based on auth state. It covers both authentication checks and authorization (feature, plan, role, permission) in one component. In a Server Component, it reads the session from the request on the server, so wrap it in <Suspense>. In a Client Component, it reads client-side auth state and needs no boundary.
Authentication check:
import { Show } from '@clerk/nextjs'
import { Suspense } from 'react'
<Suspense>
<Show when="signed-in" fallback={<p>Please sign in</p>}>
<Dashboard />
</Show>
</Suspense>Authorization checks (B2B) (wrap in <Suspense> the same way when rendered in a Server Component):
// 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.
Cache Components
New Next.js apps set cacheComponents: true in next.config (the create-next-app default since Next.js 16.4). With it, next build fails on request data read outside a <Suspense> boundary: Next.js encountered uncached or runtime data during prerendering. See Cache Components in Clerk's rendering modes guide.
Wrap
<Show>rendered in a Server Component, and any Server Component that callsauth()orcurrentUser(), in<Suspense>, as close to the auth-dependent UI as possible. Keep<ClerkProvider>and static layout outside the boundary so Next.js can prerender them.In a layout header, put one
<Suspense>inside<header>around both thesigned-outandsigned-in<Show>blocks.A page that calls
auth()orcurrentUser()to render data: move the read into an async child inside<Suspense>and keep the default-export page synchronous.A page that gates its whole render with
auth.protect(),redirectToSignIn(), orredirect(): leave the page as is and add aloading.tsxnext to it. See Read auth data inside Suspense.Wrap catch-all
<SignIn />,<SignUp />,<UserProfile />, and<OrganizationProfile />pages in<Suspense>. They read the URL withusePathname(), even in a Client Component. See Prebuilt components on catch-all routes.Never call
auth()inside'use cache'. Read the ID outside and pass it in. Seereferences/caching-auth.mdand Cache data per user with'use cache'.
Use next build --debug-prerender to list every failing route. A plain next build stops at the first.
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 |
next build fails with "uncached or runtime data during prerendering" | auth(), currentUser(), or server <Show> outside <Suspense> | Wrap the auth-dependent UI in <Suspense> (see Cache Components) |
| Cache returns wrong user's data | userId not part of the cache key | Pass userId into the 'use cache' function as an argument |
| Mutations bypass auth | Unprotected Server Action | Check auth() at start of action |
| Protected page returns 200, not 307, while signed out | The redirect happened after streaming started (e.g. under a loading.tsx) | Confirm the body redirects to sign-in and has no protected data. Keep the check in the page |
| 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 async function ExternalData() {
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>
}Render <ExternalData /> inside <Suspense>.
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
Docs
Files
- ATTRIBUTION.mdAdded by AutoGineer857 B
- SKILL.md11.7 KiB
- evals/evals.json5.3 KiB
- references/api-routes.md1.4 KiB
- references/caching-auth.md3.0 KiB
- references/middleware-strategies.md7.8 KiB
- references/server-actions.md1.6 KiB
- references/server-vs-client.md3.0 KiB
- templates/nextjs-basic-auth/app/layout.tsxScript659 B
- templates/nextjs-basic-auth/app/page.tsxScript58 B
- templates/nextjs-basic-auth/next.config.tsScript155 B
- templates/nextjs-basic-auth/package.json392 B
- templates/nextjs-basic-auth/proxy.tsScript280 B
- templates/nextjs-basic-auth/tsconfig.json612 B
references/middleware-strategies.md
Route Protection and Middleware
Filename:
proxy.ts(Next.js <=15:middleware.ts). The code is identical; only the filename changes.
Protect each resource where it runs: the page, the Route Handler, and the Server Function. clerkMiddleware() stays in proxy.ts so Clerk can read the session, but it does not decide which routes need auth.
createRouteMatcher is deprecated in @clerk/nextjs 7 and will be removed in the next major version. Don't add it. If a project already uses it, leave it working and point the user to the migration guide. Middleware matches on URL paths, so a Server Function called by ID, or a path the framework normalizes differently, can reach a protected resource without passing the check.
proxy.ts
import { clerkMiddleware } from '@clerk/nextjs/server';
export default clerkMiddleware();
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)(.*)',
],
};Keep the config.matcher block. Without the '/(api|trpc)(.*)' entry, auth() has no session on API routes.
Protect Each Resource
await auth.protect() at the top of the function. For a signed-out request it redirects to sign-in from a page, returns 401 from a Server Action, and returns 404 from a Route Handler.
A page that gates its whole render this way keeps the check at the top and gets a loading.tsx next to it. auth.protect() reads the request, and with Cache Components (cacheComponents: true, the create-next-app default), next build fails on request data read outside a <Suspense> boundary. loading.tsx wraps the page in one. See Read auth data inside Suspense.
// app/dashboard/page.tsx
import { auth } from '@clerk/nextjs/server';
export default async function Page() {
await auth.protect();
return <h1>Dashboard</h1>;
}// app/dashboard/loading.tsx
export default function Loading() {
return <p>Loading...</p>;
}// app/api/dashboard/route.ts
import { auth } from '@clerk/nextjs/server';
export async function GET() {
const { userId } = await auth.protect();
return Response.json({ userId });
}// app/dashboard/actions.ts
'use server';
import { auth } from '@clerk/nextjs/server';
export async function updateDashboard() {
await auth.protect();
// Server Function logic
}Every page checks for itself. A check in a layout is an addition, never a substitute: Next.js doesn't always re-render a layout when the page under it changes.
If auth.protect() redirects after streaming has started, such as inside <Suspense> or on a page under a loading.tsx, the response keeps status 200 and carries a client-side redirect: Next.js redirect() inserts a meta tag in a streaming context. The 200 alone doesn't prove the check ran. Confirm the response redirects to sign-in and contains no protected data, and keep the check in the page before any protected work. Don't move it into the middleware.
To return 401 from a Route Handler instead of 404, check isAuthenticated from await auth() and return the response yourself (see references/api-routes.md).
Two kinds of route skip the session check:
Webhook routes authenticate each delivery with
verifyWebhook()instead of a user session. See theclerk-webhooksskill.Deliberately public endpoints need no session check.
Core 2 ONLY (skip if current SDK): On
@clerk/nextjsv5, writeauth().protect()(synchronous). v6 and later useawait auth.protect().
Permission-Gated Resources
For B2B apps where a page or handler requires a specific permission or role, pass { permission }, { role }, or a callback to auth.protect(). A signed-in user without the permission gets a 404.
// app/invoices/page.tsx
import { auth } from '@clerk/nextjs/server';
export default async function Page() {
await auth.protect({ permission: 'org:invoices:create' });
return <InvoiceEditor />;
}// app/admin/page.tsx
import { auth } from '@clerk/nextjs/server';
export default async function Page() {
await auth.protect((has) =>
has({ role: 'org:admin' }) || has({ role: 'org:billing_manager' })
);
return <AdminPanel />;
}Add a loading.tsx next to each, as in app/dashboard above.
Prefer permissions over roles — permissions are more granular and easier to reassign across roles in the Dashboard. See the authorization checks guide.
Core 2 ONLY (skip if current SDK): On
@clerk/nextjsv5, writeauth().protect((has) => ...)(synchronous). The callback signature is the same.
Token-Based Protection (Machine APIs)
For Route Handlers that accept other token types (OAuth tokens, machine-to-machine tokens, API keys), pass a token option to auth.protect():
// app/api/machine/route.ts
import { auth } from '@clerk/nextjs/server';
export async function GET() {
const { machineId } = await auth.protect({ token: 'm2m_token' });
return Response.json({ machineId });
}Token types: 'session_token' (default, browser sessions), 'oauth_token', 'api_key', 'm2m_token', 'any' (accept any valid token). An array accepts several: auth.protect({ token: ['session_token', 'm2m_token'] }).
Core 2 ONLY (skip if current SDK): Token-type protection requires Core 3. In Core 2,
protect()only accepts a callback (notokenoption) and only validates session tokens.
Pages Router
Call getAuth(req) in getServerSideProps and in each pages/api handler, and return 401 when isAuthenticated is false. See the getAuth() reference.
Early Redirects
Signed-out users can be redirected to sign-in from proxy.ts before the page renders. That is a latency optimization only; the page still calls auth.protect(). See What about early redirects for signed-out users? in the migration guide.
Session Tasks
When session tasks are enabled (e.g., forced password reset, MFA setup), users may have a pending session status. Redirect pending users to the task page from proxy.ts, for the routes that need completed tasks:
import { clerkMiddleware } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
const sessionTaskRoutes = ['/dashboard', '/settings'];
export default clerkMiddleware(async (auth, req: NextRequest) => {
const { sessionStatus } = await auth();
const { pathname } = req.nextUrl;
const routeRequiresSessionTasks = sessionTaskRoutes.some(
(path) => pathname === path || pathname.startsWith(`${path}/`),
);
if (sessionStatus === 'pending' && routeRequiresSessionTasks) {
const url = req.nextUrl.clone();
url.pathname = '/sign-in';
return NextResponse.redirect(url);
}
});<SignIn /> renders the pending task by default, so /sign-in is enough. To host tasks on your own pages, set taskUrls on <ClerkProvider> and redirect there instead. The redirect is a navigation convenience, not a security boundary: a pending session is already treated as signed out, so each page and handler still runs its own auth.protect(). See the session tasks guide.
Core 2 ONLY (skip if current SDK):
sessionStatusis not available. Session tasks do not exist in Core 2.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Needs network access, Needs Node.js
The author says: Requires NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY. For manual JWT verification (standalone API servers without Clerk middleware), additionally requires CLERK_JWT_KEY or CLERK_PEM_PUBLIC_KEY.
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- clerk
- Publisher
- Clerk (Official)
- Source
- clerk/skills · skills/clerk-nextjs-patterns · commit c5d2c0d
- Licence
- MIT License (redistribution allowed)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by clerk or Clerk.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download clerk-nextjs-patterns.zip