Skip to content
Clerk Next.js Patterns

Clerk Next.js Patterns Skill

Clerk's official skill for advanced Next.js auth: middleware strategy, protecting Server Actions, caching-safe auth() usage, session tokens and manual JWT verification.

GitHub Docs
Development
Added to Onei
v2.2.0
MIT

Install

Install with the skills CLI

One command for Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf and dozens of other agents. Needs Node.js; the skill is fetched straight from clerk/skills.

$ npx skills add clerk/skills --skill clerk-nextjs-patterns

Run it in a terminal. The CLI finds the agents installed on your machine, asks which ones to add the skill to, and whether to install it into the folder you ran it from or for all your projects.

Other ways to install

  1. Open the skill on GitHub and download the folder that holds its SKILL.md.
  2. Put it at ~/.claude/skills/clerk-nextjs-patterns/, where Claude Code picks it up in every project.

No scripts or references next to it? Saving the SKILL.md alone as ~/.claude/skills/clerk-nextjs-patterns/SKILL.md works too.

Using another agent? Its personal skills folder
Codex
~/.agents/skills/
Cursor
~/.cursor/skills/
GitHub Copilot
~/.copilot/skills/
Gemini CLI
~/.gemini/skills/
Windsurf
~/.codeium/windsurf/skills/
OpenCode
~/.config/opencode/skills/

About

  • Public-first vs protected-first clerkMiddleware strategies
  • Protecting Next.js Server Actions
  • auth() versus client hooks, and the caching implications
  • getToken() for calling external APIs
  • Manual JWT verification for servers without Clerk middleware
  • Conditional rendering with <Show>
  • Common Next.js pitfalls and SDK version differences

Basic Clerk setup is easy; the parts that go wrong in Next.js are middleware ordering, what auth() does to caching, and how to protect Server Actions. This is Clerk's own skill for exactly those problems - the layer above "add the provider and go".

What it covers
  • Server vs client auth - when to reach for auth() and when to use the React hooks, and why mixing them up produces the bugs it does.
  • Middleware strategies - the public-first and protected-first approaches to clerkMiddleware, with the trade-offs of each spelled out.
  • Server Actions - how to protect them properly rather than assuming middleware already did.
  • Conditional rendering with <Show> for auth-dependent UI.
  • Session tokens and custom JWTs - getToken() for calling external APIs, useSession() for session data, and manual JWT verification for standalone API servers that sit outside Clerk middleware.
  • Common pitfalls - a dedicated section of the mistakes that recur in Next.js Clerk apps.

The skill also carries an SDK version table and marks Core 2-only guidance inline, so an agent working on an older SDK is not handed newer syntax.

Requirements

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY must be set. Manual JWT verification additionally needs CLERK_JWT_KEY or CLERK_PEM_PUBLIC_KEY. For first-time framework setup use the companion clerk-setup skill instead.

Where it fits

Part of Clerk's skill collection, which spans a router skill, CLI operations, custom UI, the backend REST API, framework patterns for React, React Router, Vue, Nuxt, Astro, TanStack Start and Chrome extensions, feature skills for organisations, billing, webhooks and testing, and native mobile skills for Swift, Android and Expo.

Install the whole set with npx skills add clerk/skills, add it as a Codex plugin marketplace, or clone the repo into ~/.claude/skills/clerk. Follows the Agent Skills open standard. MIT licensed.

Try it

Once installed, ask your agent something like:

Protect my Next.js Server Actions with Clerk and set up middleware for a mostly-public marketing site.

SKILL.md

The instructions your agent loads, as published by Clerk.

SKILL.md
name
clerk-nextjs-patterns
description
Advanced Next.js patterns - middleware, Server Actions, caching with Clerk.
license
MIT
allowed-tools
WebFetch
compatibility
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.
metadata
author: clerk version: 2.2.0

Next.js Patterns

Version: Check package.json for the SDK version — see clerk skill 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?
TaskReference
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 Actionsreferences/server-actions.md
API route auth (401 vs 403)references/api-routes.md
Cache auth data (user-scoped caching)references/caching-auth.md
References
ReferenceDescription
references/server-vs-client.mdawait auth() vs hooks
references/middleware-strategies.mdPublic-first vs protected-first, proxy.ts (Next.js <=15: middleware.ts)
references/server-actions.mdProtect mutations
references/api-routes.md401 vs 403
references/caching-auth.mdUser-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 !!userId pattern
  • sessionStatus — 'active' | 'pending', for detecting incomplete session tasks
  • userId, orgId, orgSlug, has(), protect() — unchanged
Core 2 ONLY (skip if current SDK): isAuthenticated and sessionStatus are not available. Check !!userId instead.
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): isAuthenticated is not available. Use if (!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. See clerk-custom-ui skill, core-3/show-component.md for the full migration table.
Common Pitfalls
SymptomCauseFix
undefined userId in Server ComponentMissing awaitawait auth() not auth()
Auth not working on API routesMissing matcherAdd `'/(api
Cache returns wrong user's dataMissing userId in keyInclude userId in unstable_cache key
Mutations bypass authUnprotected Server ActionCheck auth() at start of action
Wrong HTTP error codeConfused 401/403401 = 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: __session cookie (Clerk sets this automatically)
  • Cross-origin / mobile / API-to-API: Authorization: Bearer <token> header
CRITICAL: Always check exp and nbf claims. verifyToken from @clerk/backend handles this automatically; with raw jsonwebtoken, set ignoreExpiration: false (default) and ensure clockTolerance is minimal.
See Also
  • clerk-setup - Initial Clerk install
  • clerk-orgs - B2B patterns (active org, role/permission gating)
  • clerk-billing - Plan and feature entitlements with has()
  • clerk-webhooks - Sync user/org events to your database
  • clerk-custom-ui - Theming and customization for built-in components
Docs

Next.js SDK

Install

Scaffold authentication in a TypeScript or JavaScript app with Better Auth — the skill detects the framework, ORM and package manager first, then plans before writing code.

Browse all skills →