Clerk auth with API Keys beta (Dec 2025), Next.js 16 proxy.ts, API version 2025-11-10 breaking changes, clerkMiddleware() options, webhooks, and component reference. Use when: API keys for users/orgs, Next.js 16 middleware filename, troubleshooting JWKS/CSRF/JWT errors, webhook verification, or testing with 424242 OTP.
/plugin marketplace add jezweb/claude-skills/plugin install jezweb-tooling-skills@jezweb/claude-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
README.mdagents/clerk-setup.mdassets/example-template.txtcommands/setup.mdreferences/common-errors.mdreferences/example-reference.mdreferences/jwt-claims-guide.mdreferences/testing-guide.mdrules/clerk-auth.mdscripts/example-script.shscripts/generate-session-token.jstemplates/cloudflare/worker-auth.tstemplates/cloudflare/wrangler.jsonctemplates/jwt/advanced-template.jsontemplates/jwt/basic-template.jsontemplates/jwt/grafbase-template.jsontemplates/jwt/supabase-template.jsontemplates/nextjs/app-layout.tsxtemplates/nextjs/middleware.tstemplates/nextjs/server-component-example.tsxPackage Versions: @clerk/nextjs@6.36.7, @clerk/backend@2.29.2, @clerk/clerk-react@5.59.2, @clerk/testing@1.13.26 Breaking Changes: Nov 2025 - API version 2025-11-10, Oct 2024 - Next.js v6 async auth() Last Updated: 2026-01-09
User-scoped and organization-scoped API keys for your application. Zero-code UI component.
// 1. Add the component for self-service API key management
import { APIKeys } from '@clerk/nextjs'
export default function SettingsPage() {
return (
<div>
<h2>API Keys</h2>
<APIKeys /> {/* Full CRUD UI for user's API keys */}
</div>
)
}
Backend Verification:
import { verifyToken } from '@clerk/backend'
// API keys are verified like session tokens
const { data, error } = await verifyToken(apiKey, {
secretKey: process.env.CLERK_SECRET_KEY,
authorizedParties: ['https://yourdomain.com'],
})
// Check token type
if (data?.tokenType === 'api_key') {
// Handle API key auth
}
clerkMiddleware Token Types:
// v6.36.0+: Middleware can distinguish token types
clerkMiddleware((auth, req) => {
const { userId, tokenType } = auth()
if (tokenType === 'api_key') {
// API key auth - programmatic access
} else if (tokenType === 'session_token') {
// Regular session - web UI access
}
})
Pricing (Beta = Free):
⚠️ BREAKING: Next.js 16 changed middleware filename:
Next.js 15 and earlier: middleware.ts
Next.js 16+: proxy.ts
Correct Setup for Next.js 16:
// src/proxy.ts (NOT middleware.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)(.*)',
],
}
Administrators can mark passwords as compromised and force reset:
import { clerkClient } from '@clerk/backend'
// Force password reset for a user
await clerkClient.users.updateUser(userId, {
passwordDigest: 'compromised', // Triggers reset on next sign-in
})
Dashboard now includes org creation metrics and filtering by name/slug/date.
Affects: Applications using Clerk Billing/Commerce APIs
Critical Changes:
Endpoint URLs: /commerce/ → /billing/ (30+ endpoints)
GET /v1/commerce/plans → GET /v1/billing/plans
GET /v1/commerce/statements → GET /v1/billing/statements
POST /v1/me/commerce/checkouts → POST /v1/me/billing/checkouts
Field Terminology: payment_source → payment_method
// OLD (deprecated)
{ payment_source_id: "...", payment_source: {...} }
// NEW (required)
{ payment_method_id: "...", payment_method: {...} }
Removed Fields: Plans responses no longer include:
amount, amount_formatted (use fee.amount instead)currency, currency_symbol (use fee objects)payer_type (use for_payer_type)annual_monthly_amount, annual_amountRemoved Endpoints:
Null Handling: Explicit rules - null means "doesn't exist", omitted means "not asserting existence"
Migration: Update SDK to v6.35.0+ which includes support for API version 2025-11-10.
Official Guide: https://clerk.com/docs/guides/development/upgrading/upgrade-guides/2025-11-10
Affects: All Next.js Server Components using auth()
// ❌ OLD (v5 - synchronous)
const { userId } = auth()
// ✅ NEW (v6 - asynchronous)
const { userId } = await auth()
Also affects: auth.protect() is now async in middleware
// ❌ OLD (v5)
auth.protect()
// ✅ NEW (v6)
await auth.protect()
Compatibility: Next.js 15, 16 supported. Static rendering by default.
Custom OIDC providers and social connections now support PKCE (Proof Key for Code Exchange) for enhanced security in native/mobile applications where client secrets cannot be safely stored.
Use case: Mobile apps, native apps, public clients that can't securely store secrets.
Automatic secondary authentication when users sign in from unrecognized devices:
How it works: Clerk automatically prompts for additional verification (email code, backup code) when detecting sign-in from new device.
@clerk/nextjs v6.35.2+ includes cache invalidation improvements for Next.js 16 during sign-out.
Pattern:
import { auth } from '@clerk/nextjs/server'
export default async function Page() {
const { userId } = await auth() // ← Must await
if (!userId) {
return <div>Unauthorized</div>
}
return <div>User ID: {userId}</div>
}
CRITICAL: Always set authorizedParties to prevent CSRF attacks
import { verifyToken } from '@clerk/backend'
const { data, error } = await verifyToken(token, {
secretKey: c.env.CLERK_SECRET_KEY,
// REQUIRED: Prevent CSRF attacks
authorizedParties: ['https://yourdomain.com'],
})
Why: Without authorizedParties, attackers can use valid tokens from other domains.
Source: https://clerk.com/docs/reference/backend/verify-token
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
// Define protected routes
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/api/private(.*)',
])
const isAdminRoute = createRouteMatcher(['/admin(.*)'])
export default clerkMiddleware(async (auth, req) => {
// Protect routes
if (isProtectedRoute(req)) {
await auth.protect() // Redirects unauthenticated users
}
// Require specific permissions
if (isAdminRoute(req)) {
await auth.protect({
role: 'org:admin', // Requires organization admin role
})
}
})
| Option | Type | Description |
|---|---|---|
debug | boolean | Enable debug logging |
jwtKey | string | JWKS public key for networkless verification |
clockSkewInMs | number | Token time variance (default: 5000ms) |
organizationSyncOptions | object | URL-based org activation |
signInUrl | string | Custom sign-in URL |
signUpUrl | string | Custom sign-up URL |
clerkMiddleware({
organizationSyncOptions: {
organizationPatterns: ['/orgs/:slug', '/orgs/:slug/(.*)'],
personalAccountPatterns: ['/personal', '/personal/(.*)'],
},
})
import { Webhook } from 'svix'
export async function POST(req: Request) {
const payload = await req.text()
const headers = {
'svix-id': req.headers.get('svix-id')!,
'svix-timestamp': req.headers.get('svix-timestamp')!,
'svix-signature': req.headers.get('svix-signature')!,
}
const wh = new Webhook(process.env.CLERK_WEBHOOK_SIGNING_SECRET!)
try {
const event = wh.verify(payload, headers)
// Process event
return Response.json({ success: true })
} catch (err) {
return Response.json({ error: 'Invalid signature' }, { status: 400 })
}
}
| Event | Trigger |
|---|---|
user.created | New user signs up |
user.updated | User profile changes |
user.deleted | User account deleted |
session.created | New sign-in |
session.ended | Sign-out |
organization.created | New org created |
organization.membership.created | User joins org |
⚠️ Important: Webhook routes must be PUBLIC (no auth). Add to middleware exclude list:
const isPublicRoute = createRouteMatcher([
'/api/webhooks/clerk(.*)', // Clerk webhooks are public
])
clerkMiddleware((auth, req) => {
if (!isPublicRoute(req)) {
auth.protect()
}
})
| Component | Purpose |
|---|---|
<SignIn /> | Full sign-in flow |
<SignUp /> | Full sign-up flow |
<SignInButton /> | Trigger sign-in modal |
<SignUpButton /> | Trigger sign-up modal |
<SignedIn> | Render only when authenticated |
<SignedOut> | Render only when unauthenticated |
<UserButton /> | User menu with sign-out |
<UserProfile /> | Full profile management |
<OrganizationSwitcher /> | Switch between orgs |
<OrganizationProfile /> | Org settings |
<CreateOrganization /> | Create new org |
<APIKeys /> | API key management (NEW) |
| Hook | Returns |
|---|---|
useAuth() | { userId, sessionId, isLoaded, isSignedIn, getToken } |
useUser() | { user, isLoaded, isSignedIn } |
useClerk() | Clerk instance with methods |
useSession() | Current session object |
useOrganization() | Current org context |
useOrganizationList() | All user's orgs |
Problem: Browser cookies limited to 4KB. Clerk's default claims consume ~2.8KB, leaving 1.2KB for custom claims.
⚠️ Development Note: When testing custom JWT claims in Vite dev mode, you may encounter "431 Request Header Fields Too Large" error. This is caused by Clerk's handshake token in the URL exceeding Vite's 8KB limit. See Issue #11 for solution.
Solution:
// ✅ GOOD: Minimal claims
{
"user_id": "{{user.id}}",
"email": "{{user.primary_email_address}}",
"role": "{{user.public_metadata.role}}"
}
// ❌ BAD: Exceeds limit
{
"bio": "{{user.public_metadata.bio}}", // 6KB field
"all_metadata": "{{user.public_metadata}}" // Entire object
}
Best Practice: Store large data in database, include only identifiers/roles in JWT.
| Category | Shortcodes | Example |
|---|---|---|
| User ID & Name | {{user.id}}, {{user.first_name}}, {{user.last_name}}, {{user.full_name}} | "John Doe" |
| Contact | {{user.primary_email_address}}, {{user.primary_phone_address}} | "john@example.com" |
| Profile | {{user.image_url}}, {{user.username}}, {{user.created_at}} | "https://..." |
| Verification | {{user.email_verified}}, {{user.phone_number_verified}} | true |
| Metadata | {{user.public_metadata}}, {{user.public_metadata.FIELD}} | {"role": "admin"} |
| Organization | org_id, org_slug, org_role (in sessionClaims) | "org:admin" |
Advanced Features:
"{{user.last_name}} {{user.first_name}}""{{user.public_metadata.role || 'user'}}""{{user.public_metadata.profile.interests}}"Official Docs: https://clerk.com/docs/guides/sessions/jwt-templates
Test Emails (no emails sent, fixed OTP):
john+clerk_test@example.com
jane+clerk_test@gmail.com
Test Phone Numbers (no SMS sent, fixed OTP):
+12015550100
+19735550133
Fixed OTP Code: 424242 (works for all test credentials)
Script (scripts/generate-session-token.js):
# Generate token
CLERK_SECRET_KEY=sk_test_... node scripts/generate-session-token.js
# Create new test user
CLERK_SECRET_KEY=sk_test_... node scripts/generate-session-token.js --create-user
# Auto-refresh token every 50 seconds
CLERK_SECRET_KEY=sk_test_... node scripts/generate-session-token.js --refresh
Manual Flow:
POST /v1/usersPOST /v1/sessionsPOST /v1/sessions/{session_id}/tokensAuthorization: Bearer <token>Install @clerk/testing for automatic Testing Token management:
npm install -D @clerk/testing
Global Setup (global.setup.ts):
import { clerkSetup } from '@clerk/testing/playwright'
import { test as setup } from '@playwright/test'
setup('global setup', async ({}) => {
await clerkSetup()
})
Test File (auth.spec.ts):
import { setupClerkTestingToken } from '@clerk/testing/playwright'
import { test } from '@playwright/test'
test('sign up', async ({ page }) => {
await setupClerkTestingToken({ page })
await page.goto('/sign-up')
await page.fill('input[name="emailAddress"]', 'test+clerk_test@example.com')
await page.fill('input[name="password"]', 'TestPassword123!')
await page.click('button[type="submit"]')
// Verify with fixed OTP
await page.fill('input[name="code"]', '424242')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/dashboard')
})
Official Docs: https://clerk.com/docs/guides/development/testing/overview
This skill prevents 11 documented issues:
Error: "Missing Clerk Secret Key or API Key"
Source: https://stackoverflow.com/questions/77620604
Prevention: Always set in .env.local or via wrangler secret put
Error: "apiKey is deprecated, use secretKey"
Source: https://clerk.com/docs/upgrade-guides/core-2/backend
Prevention: Replace apiKey with secretKey in all calls
Error: "No JWK available" Source: https://github.com/clerk/javascript/blob/main/packages/backend/CHANGELOG.md Prevention: Use @clerk/backend@2.17.2 or later (fixed)
Error: No error, but CSRF vulnerability
Source: https://clerk.com/docs/reference/backend/verify-token
Prevention: Always set authorizedParties: ['https://yourdomain.com']
Error: "Cannot find module" Source: https://clerk.com/docs/upgrade-guides/core-2/backend Prevention: Update import paths for Core 2
Error: Token exceeds size limit Source: https://clerk.com/docs/backend-requests/making/custom-session-token Prevention: Keep custom claims under 1.2KB
Error: "API version v1 is deprecated" Source: https://clerk.com/docs/upgrade-guides/core-2/backend Prevention: Use latest SDK versions (API v2025-11-10)
Error: "cannot be used as a JSX component" Source: https://stackoverflow.com/questions/79265537 Prevention: Ensure React 19 compatibility with @clerk/clerk-react@5.59.2+
Error: "auth() is not a function"
Source: https://clerk.com/changelog/2024-10-22-clerk-nextjs-v6
Prevention: Always await: const { userId } = await auth()
Error: "Missing Publishable Key" or secret leaked
Prevention: Use correct prefixes (NEXT_PUBLIC_, VITE_), never commit secrets
Error: "431 Request Header Fields Too Large" when signing in
Source: Common in Vite dev mode when testing custom JWT claims
Cause: Clerk's __clerk_handshake token in URL exceeds Vite's 8KB header limit
Prevention:
Add to package.json:
{
"scripts": {
"dev": "NODE_OPTIONS='--max-http-header-size=32768' vite"
}
}
Temporary Workaround: Clear browser cache, sign out, sign back in
Why: Clerk dev tokens are larger than production; custom JWT claims increase handshake token size
Note: This is different from Issue #6 (session token size). Issue #6 is about cookies (1.2KB), this is about URL parameters in dev mode (8KB → 32KB).
/clerk/clerk-docsLatest (Nov 22, 2025):
{
"dependencies": {
"@clerk/nextjs": "^6.36.7",
"@clerk/clerk-react": "^5.59.2",
"@clerk/backend": "^2.29.2",
"@clerk/testing": "^1.13.26"
}
}
Token Efficiency:
Errors prevented: 11 documented issues with exact solutions Key value: API Keys beta, Next.js 16 proxy.ts, clerkMiddleware() options, webhooks, component reference, API 2025-11-10 breaking changes, JWT size limits
Last verified: 2026-01-03 | Skill version: 3.0.0 | Changes: Added API Keys beta (Dec 2025), Next.js 16 proxy.ts filename, clerkMiddleware() configuration options, webhooks verification patterns, UI components + React hooks reference tables, organization sync patterns.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.