Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add phrazzld/claude-config --skill "clerk-auth"
Install specific skill from multi-skill repository
# Description
Clerk authentication integration patterns for Next.js and Convex. Invoke for: user authentication, session management, JWT templates, webhook handling, middleware configuration, protected routes, Convex auth integration.
# SKILL.md
name: clerk-auth
description: "Clerk authentication integration patterns for Next.js and Convex. Invoke for: user authentication, session management, JWT templates, webhook handling, middleware configuration, protected routes, Convex auth integration."
Clerk Authentication Patterns
Authentication integration patterns for Clerk with Next.js and Convex backends.
Core Concepts
Authentication Flow
- User authenticates via Clerk (sign-in/sign-up)
- Clerk issues session token (JWT)
- Frontend passes token to backend
- Backend verifies token and extracts user identity
Key Components
- Clerk Dashboard: User management, JWT templates, webhooks
- @clerk/nextjs: React hooks, middleware, components
- Convex auth:
ctx.auth.getUserIdentity()for backend verification
Next.js Setup
Middleware
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/api/webhooks(.*)', // Webhooks must be public
])
export default clerkMiddleware((auth, req) => {
if (!isPublicRoute(req)) {
auth().protect()
}
})
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}
Environment Variables
# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxx
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
Critical: Set on BOTH Vercel and local. Mismatch causes silent failures.
Convex Integration
JWT Template (Clerk Dashboard)
Create JWT template named convex:
{
"sub": "{{user.id}}",
"iss": "https://clerk.your-domain.com",
"email": "{{user.primary_email_address}}",
"name": "{{user.full_name}}"
}
Template name is case-sensitive. Must match convex/auth.config.ts.
Convex Auth Config
// convex/auth.config.ts
export default {
providers: [
{
domain: process.env.CLERK_JWT_ISSUER_DOMAIN,
applicationID: "convex",
},
],
}
Backend User Identity
// convex/users.ts
import { query, mutation } from "./_generated/server"
export const getCurrentUser = query({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) return null
// identity contains JWT claims:
// - subject (Clerk user ID)
// - email
// - name
return identity
},
})
Webhook Handling
Webhook URL
Configure in Clerk Dashboard → Webhooks:
- URL: https://your-app.com/api/webhooks/clerk
- Events: user.created, user.updated, user.deleted
Webhook Handler
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix'
import { headers } from 'next/headers'
import { WebhookEvent } from '@clerk/nextjs/server'
export async function POST(req: Request) {
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET
if (!WEBHOOK_SECRET) {
throw new Error('Missing CLERK_WEBHOOK_SECRET')
}
const headerPayload = headers()
const svix_id = headerPayload.get('svix-id')
const svix_timestamp = headerPayload.get('svix-timestamp')
const svix_signature = headerPayload.get('svix-signature')
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response('Missing svix headers', { status: 400 })
}
const payload = await req.json()
const body = JSON.stringify(payload)
const wh = new Webhook(WEBHOOK_SECRET)
let evt: WebhookEvent
try {
evt = wh.verify(body, {
'svix-id': svix_id,
'svix-timestamp': svix_timestamp,
'svix-signature': svix_signature,
}) as WebhookEvent
} catch (err) {
console.error('Webhook verification failed:', err)
return new Response('Invalid signature', { status: 400 })
}
// Handle events
switch (evt.type) {
case 'user.created':
// Sync user to database
break
case 'user.updated':
// Update user in database
break
case 'user.deleted':
// Handle user deletion
break
}
return new Response('OK', { status: 200 })
}
Common Issues
"Unauthenticated" errors
- Check JWT template name matches config
- Verify
CLERK_JWT_ISSUER_DOMAINis set - Ensure middleware isn't blocking auth routes
Webhook failures
- Verify
CLERK_WEBHOOK_SECRETis set (not just locally) - Check webhook URL uses canonical domain (no redirects)
- Ensure
/api/webhooks/*is in public routes
Session not persisting
- Check cookies are being set (dev tools)
- Verify domain configuration in Clerk Dashboard
- Ensure HTTPS in production
Best Practices
- Sync users via webhooks, not on-demand
- Store Clerk ID as foreign key in your database
- Use
currentUser()for server components - Use
useUser()for client components - Protect API routes with
auth()in route handlers - Keep JWT templates minimal (only needed claims)
References
references/convex-integration.md— Detailed Convex auth patternsreferences/webhook-events.md— Clerk webhook event typesreferences/troubleshooting.md— Common issues and solutions
Related Skills
billing-security— Payment and auth security patternsexternal-integration-patterns— General external service integrationenv-var-hygiene— Environment variable management
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.