**Status**: Production Ready ✅
/plugin marketplace add secondsky/claude-skills/plugin install clerk-auth@claude-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/example-template.txtreferences/common-errors.mdreferences/jwt-claims-guide.mdreferences/testing-guide.mdscripts/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.tsxtemplates/react/App.tsxtemplates/react/main.tsxtemplates/typescript/custom-jwt-types.d.tstemplates/vite/package.jsonStatus: Production Ready ✅ Last Updated: 2025-11-21 Dependencies: None Latest Versions: @clerk/nextjs@6.35.3, @clerk/backend@2.17.2, @clerk/clerk-react@5.51.0, @clerk/testing@1.4.4
Choose your framework:
```bash bun add @clerk/clerk-react ```
Latest Version: @clerk/clerk-react@5.51.0 (verified 2025-10-22)
Update `src/main.tsx`:
```typescript import React from 'react' import ReactDOM from 'react-dom/client' import { ClerkProvider } from '@clerk/clerk-react' import App from './App.tsx' import './index.css'
// Get publishable key from environment const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
if (!PUBLISHABLE_KEY) { throw new Error('Missing Publishable Key') }
ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <ClerkProvider publishableKey={PUBLISHABLE_KEY}> <App /> </ClerkProvider> </React.StrictMode>, ) ```
CRITICAL:
Create `.env.local`:
```bash VITE_CLERK_PUBLISHABLE_KEY=pk_test_... ```
Security Note: Only `VITE_` prefixed vars are exposed to client code.
```typescript import { useUser, useAuth, useClerk } from '@clerk/clerk-react'
function App() { // Get user object (includes email, metadata, etc.) const { isLoaded, isSignedIn, user } = useUser()
// Get auth state and session methods const { userId, sessionId, getToken } = useAuth()
// Get Clerk instance for advanced operations const { openSignIn, signOut } = useClerk()
// Always check isLoaded before rendering auth-dependent UI if (!isLoaded) { return <div>Loading...</div> }
if (!isSignedIn) { return <button onClick={() => openSignIn()}>Sign In</button> }
return ( <div> <h1>Welcome {user.firstName}!</h1> <p>Email: {user.primaryEmailAddress?.emailAddress}</p> <button onClick={() => signOut()}>Sign Out</button> </div> ) } ```
Why This Matters:
```bash bun add @clerk/nextjs ```
Latest Version: @clerk/nextjs@6.33.3 (verified 2025-10-22)
Create `.env.local`:
```bash NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding ```
CRITICAL:
Create `middleware.ts` in project root:
```typescript import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
// Define which routes are public (everything else requires auth) const isPublicRoute = createRouteMatcher([ '/', '/sign-in(.)', '/sign-up(.)', '/api/webhooks(.*)', // Clerk webhooks should be public ])
export default clerkMiddleware(async (auth, request) => { // Protect all routes except public ones if (!isPublicRoute(request)) { await auth.protect() } })
export const config = { matcher: [ // Skip Next.js internals and static files '/((?!_next|[^?]\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).)', // Always run for API routes '/(api|trpc)(.*)', ], } ```
CRITICAL:
Update `app/layout.tsx`:
```typescript import { ClerkProvider } from '@clerk/nextjs' import './globals.css'
export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <ClerkProvider> <html lang="en"> <body>{children}</body> </html> </ClerkProvider> ) } ```
```typescript import { auth, currentUser } from '@clerk/nextjs/server'
export default async function DashboardPage() { // Get auth state (lightweight) const { userId, sessionId } = await auth()
// Get full user object (heavier, fewer calls) const user = await currentUser()
if (!userId) { return <div>Unauthorized</div> }
return ( <div> <h1>Dashboard</h1> <p>User ID: {userId}</p> <p>Email: {user?.primaryEmailAddress?.emailAddress}</p> </div> ) } ```
CRITICAL:
```bash bun add @clerk/backend hono ```
Latest Versions:
Create `.dev.vars` for local development:
```bash CLERK_SECRET_KEY=sk_test_... CLERK_PUBLISHABLE_KEY=pk_test_... ```
Production: Use `wrangler secret put CLERK_SECRET_KEY`
Create `src/index.ts`:
```typescript import { Hono } from 'hono' import { verifyToken } from '@clerk/backend'
type Bindings = { CLERK_SECRET_KEY: string CLERK_PUBLISHABLE_KEY: string }
type Variables = { userId: string | null sessionClaims: any | null }
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
// Middleware: Verify Clerk token app.use('/api/*', async (c, next) => { const authHeader = c.req.header('Authorization')
if (!authHeader) { c.set('userId', null) c.set('sessionClaims', null) return next() }
const token = authHeader.replace('Bearer ', '')
try { const { data, error } = await verifyToken(token, { secretKey: c.env.CLERK_SECRET_KEY, // IMPORTANT: Set authorizedParties to prevent CSRF attacks authorizedParties: ['https://yourdomain.com'], })
if (error) {
console.error('Token verification failed:', error)
c.set('userId', null)
c.set('sessionClaims', null)
} else {
c.set('userId', data.sub)
c.set('sessionClaims', data)
}
} catch (err) { console.error('Token verification error:', err) c.set('userId', null) c.set('sessionClaims', null) }
return next() })
// Protected route app.get('/api/protected', (c) => { const userId = c.get('userId')
if (!userId) { return c.json({ error: 'Unauthorized' }, 401) }
return c.json({ message: 'This is protected', userId, sessionClaims: c.get('sessionClaims'), }) })
export default app ```
CRITICAL:
Clerk allows customizing JWT (JSON Web Token) structure using templates. This enables integration with third-party services, role-based access control, and multi-tenant applications.
1. Navigate to Clerk Dashboard:
2. Define Template:
{
"user_id": "{{user.id}}",
"email": "{{user.primary_email_address}}",
"role": "{{user.public_metadata.role || 'user'}}"
}
3. Use Template in Code:
// Frontend (React/Next.js)
const { getToken } = useAuth()
const token = await getToken({ template: 'my-template' })
// Backend (Cloudflare Workers)
const sessionClaims = c.get('sessionClaims')
const role = sessionClaims?.role
| 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" |
String Interpolation:
{
"full_name": "{{user.last_name}} {{user.first_name}}",
"greeting": "Hello, {{user.first_name}}!"
}
Conditional Fallbacks:
{
"role": "{{user.public_metadata.role || 'user'}}",
"age": "{{user.public_metadata.age || 18}}",
"verified": "{{user.email_verified || user.phone_number_verified}}"
}
Nested Metadata with Dot Notation:
{
"interests": "{{user.public_metadata.profile.interests}}",
"department": "{{user.public_metadata.department}}"
}
Every JWT includes these claims automatically (cannot be overridden):
{
"azp": "http://localhost:3000", // Authorized party
"exp": 1639398300, // Expiration time
"iat": 1639398272, // Issued at
"iss": "https://your-app.clerk.accounts.dev", // Issuer
"jti": "10db7f531a90cb2faea4", // JWT ID
"nbf": 1639398220, // Not before
"sub": "user_1deJLArSTiWiF1YdsEWysnhJLLY" // User ID
}
Problem: Browser cookies limited to 4KB. Clerk's default claims consume ~2.8KB, leaving 1.2KB for custom claims.
⚠️ Development Note: When testing custom 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.
Add global type declarations for auto-complete:
Create types/globals.d.ts:
export {}
declare global {
interface CustomJwtSessionClaims {
metadata: {
role?: 'admin' | 'moderator' | 'user'
onboardingComplete?: boolean
organizationId?: string
}
}
}
Role-Based Access Control:
{
"email": "{{user.primary_email_address}}",
"role": "{{user.public_metadata.role || 'user'}}",
"permissions": "{{user.public_metadata.permissions}}"
}
Multi-Tenant Applications:
{
"user_id": "{{user.id}}",
"org_id": "{{user.public_metadata.org_id}}",
"org_role": "{{user.public_metadata.org_role}}"
}
Supabase Integration:
{
"email": "{{user.primary_email_address}}",
"app_metadata": {
"provider": "clerk"
},
"user_metadata": {
"full_name": "{{user.full_name}}"
}
}
references/jwt-claims-guide.md for comprehensive documentationtemplates/jwt/ directory for working examplestemplates/typescript/custom-jwt-types.d.tsClerk provides comprehensive testing tools for local development and CI/CD pipelines.
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)
For testing API endpoints, generate valid session tokens (60-second lifetime):
# Using the provided script
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:
bun add -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')
})
Testing Tokens bypass bot detection in test suites.
Obtain Token:
curl -X POST https://api.clerk.com/v1/testing_tokens \
-H "Authorization: Bearer sk_test_..."
Use in Frontend API Requests:
POST https://your-app.clerk.accounts.dev/v1/client/sign_ups?__clerk_testing_token=TOKEN
Note: @clerk/testing handles this automatically for Playwright/Cypress.
Testing Tokens work in both development and production, but:
references/testing-guide.md for comprehensive testing documentationscripts/generate-session-token.jsThis 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-04-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.51.0+
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:
```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).
✅ Set `authorizedParties` when verifying tokens ✅ Use `CLERK_SECRET_KEY` environment variable ✅ Check `isLoaded` before rendering auth UI ✅ Use `getToken()` fresh for each request ✅ Await `auth()` in Next.js v6+ ✅ Use `NEXT_PUBLIC_` prefix for client vars only ✅ Store secrets via `wrangler secret put` ✅ Implement middleware for route protection ✅ Use API version 2025-04-10 or later
❌ Store `CLERK_SECRET_KEY` in client code ❌ Use deprecated `apiKey` parameter ❌ Store tokens in localStorage ❌ Skip `authorizedParties` check ❌ Exceed 1.2KB for custom JWT claims ❌ Forget to check `isLoaded` ❌ Expose secrets with `NEXT_PUBLIC_` prefix ❌ Use API version v1
| Reference | Load When... |
|---|---|
common-errors.md | Debugging authentication failures, token issues, or 401/403 errors |
jwt-claims-guide.md | Setting up custom JWT claims, RBAC, multi-tenant auth, or Supabase integration |
testing-guide.md | Writing E2E tests with Playwright, generating test session tokens |
```json { "dependencies": { "@clerk/nextjs": "^6.33.3", "@clerk/clerk-react": "^5.51.0", "@clerk/backend": "^2.17.2" } } ```
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.