clerk-nextjs-patterns

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 and protecting Server Actions from unauthorized mutations Provides Core 2 compatibility notes throughout; check package.json for SDK version and follow version-specific guidance on isAuthenticated , sessionStatus , and component APIs

INSTALLATION
npx skills add https://github.com/clerk/skills --skill clerk-nextjs-patterns
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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?

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 !!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

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

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 &#x26; 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

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card