Generated banner for Clerk Nextjs Patterns
Generated logo for Clerk Nextjs Patterns

Clerk Nextjs Patterns

ClerkOfficialCreated by clerk

  • Skill
  • Tool integration
  • Software development
  • MIT
  • Low risk
  • Has scripts

From the author

Advanced Next.js patterns - middleware, Server Actions, caching with Clerk.
— the description by clerk

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/clerk/clerk-nextjs-patterns/download
  2. Unzip 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.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.

Route protection: call await auth.protect() in each page, Route Handler, and Server Function. Keep clerkMiddleware() bare. 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.

What Do You Need?

TaskReference
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 Actionsreferences/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 ComponentsCache Components

References

ReferenceDescription
references/server-vs-client.mdawait auth() vs hooks
references/middleware-strategies.mdauth.protect() in each resource, bare clerkMiddleware() in proxy.ts (Next.js <=15: middleware.ts)
references/server-actions.mdProtect mutations
references/api-routes.md401 vs 403
references/caching-auth.mdUser-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 !!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'
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): isAuthenticated is not available. Use if (!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. See clerk-custom-ui skill, core-3/show-component.md for 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 calls auth() or currentUser(), 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 the signed-out and signed-in <Show> blocks.

  • A page that calls auth() or currentUser() 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(), or redirect(): leave the page as is and add a loading.tsx next to it. See Read auth data inside Suspense.

  • Wrap catch-all <SignIn />, <SignUp />, <UserProfile />, and <OrganizationProfile /> pages in <Suspense>. They read the URL with usePathname(), 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. See references/caching-auth.md and 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

SymptomCauseFix
undefined userId in Server ComponentMissing awaitawait auth() not auth()
Auth not working on API routesMissing matcherAdd `'/(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 datauserId not part of the cache keyPass userId into the 'use cache' function as an argument
Mutations bypass authUnprotected Server ActionCheck auth() at start of action
Protected page returns 200, not 307, while signed outThe 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 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 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: __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

Files

evals/evals.json

{
  "skill_name": "clerk-nextjs-patterns",
  "evals": [
    {
      "id": 1,
      "prompt": "my dashboard page uses useAuth and useUser hooks with useEffect for redirect. this causes a flash of unauthenticated content. convert it to a server component using auth() from @clerk/nextjs/server instead.",
      "expected_output": "Converts dashboard from client component with hooks to server component with auth(), removes useEffect redirect pattern",
      "scaffold": "nextjs-basic-auth",
      "assertions": [
        "Removes 'use client' directive from dashboard page",
        "Replaces useAuth/useUser hooks with auth() from @clerk/nextjs/server",
        "Uses server-side redirect() from next/navigation instead of useEffect + router.push",
        "Removes useEffect, useRouter, isLoaded checks",
        "Page still displays user data (userId from auth or currentUser)"
      ]
    },
    {
      "id": 2,
      "prompt": "my api route at app/api/data/route.ts has no auth protection. anyone can access the data. add clerk auth to this api route so only authenticated users can get the data, and return 401 for unauthenticated requests.",
      "expected_output": "Adds auth() check to API route, returns 401 for unauthenticated requests",
      "scaffold": "nextjs-basic-auth",
      "assertions": [
        "Imports auth from @clerk/nextjs/server",
        "Calls auth() at the top of the GET handler",
        "Returns 401 status when userId is null",
        "Only returns data when user is authenticated",
        "Does NOT use client-side hooks in the API route"
      ]
    },
    {
      "id": 3,
      "prompt": "my homepage is a client component that uses useUser to check if the user is signed in. convert it to use clerk's Show component pattern instead so it can be a server component with conditional rendering.",
      "expected_output": "Converts home page to use Show (preferred in Core 3) or SignedIn/SignedOut (Core 2) components instead of useUser hook",
      "scaffold": "nextjs-basic-auth",
      "assertions": [
        "Removes 'use client' directive from home page",
        "Removes useUser hook import and usage",
        "Uses Show with when='signed-in' (preferred) OR SignedIn/SignedOut components from @clerk/nextjs for conditional rendering",
        "Preserves the same visual output (welcome message when signed in, sign-in prompt when not)",
        "Page works as a server component",
        "Wraps the server-rendered Show in <Suspense>, so next build passes with cacheComponents: true"
      ]
    },
    {
      "id": 4,
      "prompt": "add a members-only /dashboard section: a page, an api route at /api/dashboard that returns the dashboard data, and a server action that saves a dashboard setting. signed-out visitors should be sent to sign-in. / and /sign-in stay public.",
      "expected_output": "Adds auth.protect() (or an auth() check with a redirect) in the dashboard page, an auth check in the /api/dashboard route handler, and an auth check at the top of the Server Action. proxy.ts keeps a bare clerkMiddleware().",
      "scaffold": "nextjs-basic-auth",
      "assertions": [
        "Does NOT import or call createRouteMatcher",
        "The dashboard page itself (or an async child it renders) calls await auth.protect(), or checks auth() and redirects to sign-in",
        "The dashboard page reads auth inside <Suspense> or next to a loading.tsx, so next build passes with cacheComponents: true",
        "The /api/dashboard route handler checks auth itself and rejects signed-out requests",
        "The Server Action rejects signed-out calls before it reads protected data or performs any mutation",
        "/ and /sign-in remain reachable while signed out",
        "proxy.ts (or middleware.ts) still exports clerkMiddleware()"
      ]
    },
    {
      "id": 5,
      "prompt": "i need to call an external API (Hasura GraphQL) from my Next.js server component. get a custom JWT using a template called 'hasura' and pass it in the Authorization header.",
      "expected_output": "Uses getToken with template parameter from auth() server-side, passes as Bearer token to external API",
      "scaffold": "nextjs-basic-auth",
      "assertions": [
        "Uses getToken with a template parameter (e.g. 'hasura') to get a custom JWT",
        "Uses either auth() server-side or useAuth() client-side to access getToken",
        "Passes the token as Authorization Bearer header to the external API call",
        "Handles the case where getToken returns null (user not authenticated)"
      ]
    },
    {
      "id": 6,
      "prompt": "i have a standalone Node.js API server that receives requests with a Clerk session token in the Authorization header. i need to verify the JWT signature and check if the user is authenticated. i don't want to use Clerk middleware, just manual verification.",
      "expected_output": "Manually verifies Clerk JWT using jsonwebtoken or @clerk/backend verifyToken, checks claims",
      "scaffold": "nextjs-basic-auth",
      "assertions": [
        "Extracts the token from the Authorization Bearer header",
        "Verifies the token using either jsonwebtoken with CLERK_PEM_PUBLIC_KEY or @clerk/backend verifyToken",
        "Checks token expiration (exp claim) and not-before (nbf claim)",
        "Returns 401 for invalid or expired tokens",
        "Returns the decoded claims or user data on successful verification"
      ]
    }
  ]
}

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.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download clerk-nextjs-patterns.zip