# Clerk JWT Claims: Complete Reference **Last Updated**: 2025-10-22 **Clerk API Version**: 2025-04-10 **Status**: Production Ready ✅ --- ## Overview This guide provides comprehensive documentation for all JWT claims available in Clerk, including default claims, user property shortcodes, organization claims, metadata access patterns, and advanced template features. **Use this guide when**: - Creating custom JWT templates in Clerk Dashboard - Integrating with third-party services requiring specific token formats - Implementing role-based access control (RBAC) - Building multi-tenant applications - Accessing user data in backend services --- ## Table of Contents 1. [Default Claims (Auto-Included)](#default-claims-auto-included) 2. [User Property Shortcodes](#user-property-shortcodes) 3. [Organization Claims](#organization-claims) 4. [Metadata Access](#metadata-access) 5. [Advanced Template Features](#advanced-template-features) 6. [Creating JWT Templates](#creating-jwt-templates) 7. [TypeScript Type Safety](#typescript-type-safety) 8. [Common Use Cases](#common-use-cases) 9. [Limitations & Gotchas](#limitations--gotchas) 10. [Official Documentation](#official-documentation) --- ## Default Claims (Auto-Included) Every JWT generated by Clerk automatically includes these claims. **These cannot be overridden in custom templates**. ```json { "azp": "http://localhost:3000", // Authorized party (your app URL) "exp": 1639398300, // Expiration time (Unix timestamp) "iat": 1639398272, // Issued at (Unix timestamp) "iss": "https://your-app.clerk.accounts.dev", // Issuer (Clerk instance URL) "jti": "10db7f531a90cb2faea4", // JWT ID (unique identifier) "nbf": 1639398220, // Not before (Unix timestamp) "sub": "user_1deJLArSTiWiF1YdsEWysnhJLLY" // Subject (user ID) } ``` ### Claim Descriptions | Claim | Name | Description | Can Override? | |-------|------|-------------|---------------| | `azp` | Authorized Party | The URL of your application | ❌ No | | `exp` | Expiration Time | When the token expires (Unix timestamp) | ❌ No | | `iat` | Issued At | When the token was created (Unix timestamp) | ❌ No | | `iss` | Issuer | Your Clerk instance URL | ❌ No | | `jti` | JWT ID | Unique identifier for this token | ❌ No | | `nbf` | Not Before | Token not valid before this time (Unix timestamp) | ❌ No | | `sub` | Subject | User ID (same as `userId` in auth objects) | ❌ No | **IMPORTANT**: These claims consume approximately **200-300 bytes** of the 4KB cookie limit, leaving ~1.2KB for custom claims. --- ## User Property Shortcodes Shortcodes are placeholder strings that Clerk replaces with actual user data during token generation. Use double curly braces: `{{shortcode}}`. ### Basic User Properties ```json { "user_id": "{{user.id}}", // User's unique ID "first_name": "{{user.first_name}}", // User's first name (or null) "last_name": "{{user.last_name}}", // User's last name (or null) "full_name": "{{user.full_name}}", // User's full name (or null) "email": "{{user.primary_email_address}}", // Primary email address "phone": "{{user.primary_phone_address}}", // Primary phone number (or null) "avatar": "{{user.image_url}}", // User's profile image URL "created_at": "{{user.created_at}}", // Account creation timestamp "username": "{{user.username}}", // Username (if enabled) "email_verified": "{{user.email_verified}}", // Boolean: email verified? "phone_verified": "{{user.phone_number_verified}}" // Boolean: phone verified? } ``` ### Verification Status ```json { "has_verified_email": "{{user.email_verified}}", "has_verified_phone": "{{user.phone_number_verified}}", "has_verified_contact": "{{user.email_verified || user.phone_number_verified}}" } ``` ### Complete User Shortcode Reference | Shortcode | Type | Description | Example Value | |-----------|------|-------------|---------------| | `{{user.id}}` | string | User's unique identifier | `"user_2abc..."` | | `{{user.first_name}}` | string \| null | First name | `"John"` | | `{{user.last_name}}` | string \| null | Last name | `"Doe"` | | `{{user.full_name}}` | string \| null | Full name (computed) | `"John Doe"` | | `{{user.primary_email_address}}` | string | Primary email | `"john@example.com"` | | `{{user.primary_phone_address}}` | string \| null | Primary phone | `"+1234567890"` | | `{{user.image_url}}` | string | Profile image URL | `"https://..."` | | `{{user.created_at}}` | number | Unix timestamp | `1639398272` | | `{{user.username}}` | string \| null | Username (if enabled) | `"johndoe"` | | `{{user.email_verified}}` | boolean | Email verified? | `true` | | `{{user.phone_number_verified}}` | boolean | Phone verified? | `false` | | `{{user.public_metadata}}` | object | All public metadata | `{...}` | | `{{user.unsafe_metadata}}` | object | All unsafe metadata | `{...}` | --- ## Organization Claims Clerk includes claims for the **active organization** (if user is in one and has selected it). ### Active Organization Claims ```json { "org_id": "org_2abc...", // Active organization ID "org_slug": "acme-corp", // Active organization slug "org_role": "org:admin" // User's role in active organization } ``` **CRITICAL CHANGE (Core 2)**: - ✅ **New**: `org_id`, `org_slug`, `org_role` (active org only) - ❌ **Removed**: `orgs` claim (previously contained all user organizations) ### Accessing Organization Data in Templates ```json { "organization": { "id": "{{user.public_metadata.org_id}}", "name": "{{user.public_metadata.org_name}}", "role": "{{user.public_metadata.org_role}}" } } ``` **Note**: For all organizations (not just active), you must: 1. Store org data in `user.public_metadata` via Clerk API 2. Use custom JWT template to include it --- ## Metadata Access Clerk provides two metadata fields for storing custom user data: ### Public Metadata - ✅ Accessible on client side - ✅ Included in user objects - ✅ Can be included in JWT templates - ❌ Should NOT contain sensitive data ### Unsafe Metadata - ⚠️ Name is misleading - it's for "unvalidated" data - ✅ Accessible on client side - ✅ Can be included in JWT templates - ❌ Should NOT contain sensitive data ### Basic Metadata Access ```json { "all_public": "{{user.public_metadata}}", "all_unsafe": "{{user.unsafe_metadata}}" } ``` **Result**: ```json { "all_public": { "role": "admin", "department": "engineering" }, "all_unsafe": { "onboardingComplete": true } } ``` ### Nested Metadata with Dot Notation For nested objects, use dot notation to access specific fields: **User's public_metadata**: ```json { "profile": { "interests": ["hiking", "knitting"], "bio": "Software engineer passionate about..." }, "addresses": { "Home": "2355 Pointe Lane, 56301 Minnesota", "Work": "3759 Newton Street, 33487 Florida" }, "role": "admin", "department": "engineering" } ``` **JWT Template**: ```json { "role": "{{user.public_metadata.role}}", "department": "{{user.public_metadata.department}}", "interests": "{{user.public_metadata.profile.interests}}", "home_address": "{{user.public_metadata.addresses.Home}}" } ``` **Generated Token**: ```json { "role": "admin", "department": "engineering", "interests": ["hiking", "knitting"], "home_address": "2355 Pointe Lane, 56301 Minnesota" } ``` **Why This Matters**: Dot notation prevents including the entire metadata object, reducing token size. --- ## Advanced Template Features ### String Interpolation Combine multiple shortcodes into a single string: ```json { "full_name": "{{user.last_name}} {{user.first_name}}", "greeting": "Hello, {{user.first_name}}!", "email_with_name": "{{user.full_name}} <{{user.primary_email_address}}>" } ``` **Result**: ```json { "full_name": "Doe John", "greeting": "Hello, John!", "email_with_name": "John Doe " } ``` **IMPORTANT**: Interpolated values are **always strings**, even if null values are present. ### Conditional Expressions (Fallbacks) Use `||` operator to provide fallback values: ```json { "full_name": "{{user.full_name || 'Guest User'}}", "age": "{{user.public_metadata.age || 18}}", "role": "{{user.public_metadata.role || 'user'}}", "verified": "{{user.email_verified || user.phone_number_verified}}" } ``` **How It Works**: - Returns first **non-falsy** operand - Final operand serves as default - String literals require **single quotes**: `'default'` - Can chain multiple fallbacks **Example with Multiple Fallbacks**: ```json { "age": "{{user.public_metadata.age || user.unsafe_metadata.age || 18}}" } ``` This checks: 1. `user.public_metadata.age` (if null/false, continue) 2. `user.unsafe_metadata.age` (if null/false, continue) 3. `18` (default value) ### Boolean Checks ```json { "has_verified_contact": "{{user.email_verified || user.phone_number_verified}}", "is_complete": "{{user.public_metadata.profileComplete || false}}" } ``` **Result**: ```json { "has_verified_contact": true, // If either email or phone is verified "is_complete": false // If profileComplete is not set } ``` --- ## Creating JWT Templates ### Step 1: Navigate to Clerk Dashboard 1. Go to **Sessions** page in Clerk Dashboard 2. Click **Customize session token** 3. Choose **Create template** or use pre-built templates ### Step 2: Configure Template Properties **Template Properties**: ```json { "name": "supabase", // Unique identifier (lowercase, no spaces) "lifetime": 3600, // Token expiration in seconds (default: 60) "allowed_clock_skew": 5, // Leeway for clock differences (default: 5) "claims": { // Your custom claims "email": "{{user.primary_email_address}}", "role": "{{user.public_metadata.role}}" } } ``` ### Step 3: Use Template in Code **React / Next.js**: ```typescript import { useAuth } from '@clerk/nextjs' function MyComponent() { const { getToken } = useAuth() // Get token with custom template const token = await getToken({ template: 'supabase' }) // Use token to authenticate with external service const response = await fetch('https://api.supabase.com/endpoint', { headers: { 'Authorization': `Bearer ${token}` } }) } ``` **Cloudflare Workers**: ```typescript import { clerkClient } from '@clerk/backend' // Generate token for user const token = await clerkClient.users.getUserOauthAccessToken( userId, 'supabase' // Template name ) ``` --- ## TypeScript Type Safety Add type safety for custom claims with global declarations: **Create `types/globals.d.ts`**: ```typescript export {} declare global { interface CustomJwtSessionClaims { metadata: { role?: 'admin' | 'moderator' | 'user' onboardingComplete?: boolean department?: string organizationId?: string } } } ``` **Usage**: ```typescript import { auth } from '@clerk/nextjs/server' export default async function Page() { const { sessionClaims } = await auth() // TypeScript now knows about these properties const role = sessionClaims?.metadata?.role // Type: 'admin' | 'moderator' | 'user' | undefined const isComplete = sessionClaims?.metadata?.onboardingComplete // Type: boolean | undefined } ``` --- ## Common Use Cases ### 1. Role-Based Access Control (RBAC) **JWT Template**: ```json { "email": "{{user.primary_email_address}}", "role": "{{user.public_metadata.role || 'user'}}", "permissions": "{{user.public_metadata.permissions}}" } ``` **Backend Verification**: ```typescript app.get('/api/admin', async (c) => { const sessionClaims = c.get('sessionClaims') if (sessionClaims?.role !== 'admin') { return c.json({ error: 'Forbidden' }, 403) } return c.json({ message: 'Admin data' }) }) ``` ### 2. Multi-Tenant Applications **JWT Template**: ```json { "user_id": "{{user.id}}", "email": "{{user.primary_email_address}}", "org_id": "{{user.public_metadata.org_id}}", "org_slug": "{{user.public_metadata.org_slug}}", "org_role": "{{user.public_metadata.org_role}}" } ``` **Usage**: ```typescript // Filter data by organization const data = await db.query('SELECT * FROM items WHERE org_id = ?', [ sessionClaims.org_id ]) ``` ### 3. Supabase Integration **JWT Template** (Name: `supabase`): ```json { "email": "{{user.primary_email_address}}", "app_metadata": { "provider": "clerk" }, "user_metadata": { "full_name": "{{user.full_name}}", "avatar_url": "{{user.image_url}}" } } ``` **Usage**: ```typescript import { createClient } from '@supabase/supabase-js' const token = await getToken({ template: 'supabase' }) const supabase = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, { global: { headers: { Authorization: `Bearer ${token}` } } } ) ``` ### 4. Grafbase GraphQL Integration **JWT Template** (Name: `grafbase`): ```json { "sub": "{{user.id}}", "groups": ["org:admin", "org:member"] } ``` **Grafbase Configuration**: ```toml [auth.providers.clerk] issuer = "https://your-app.clerk.accounts.dev" jwks = "https://your-app.clerk.accounts.dev/.well-known/jwks.json" ``` --- ## Limitations & Gotchas ### 1. Token Size Limit: 1.2KB for Custom Claims **Problem**: Browsers limit cookies to 4KB. Clerk's default claims consume ~2.8KB, leaving **1.2KB for custom claims**. **Solution**: - ✅ Use JWT for minimal claims (user ID, role, email) - ✅ Store large data (bio, preferences) in your database - ✅ Fetch additional data after authentication - ❌ Don't include large objects or arrays **Bad Example** (Exceeds limit): ```json { "email": "{{user.primary_email_address}}", "bio": "{{user.public_metadata.bio}}", // 6KB bio field "all_metadata": "{{user.public_metadata}}" // Entire object } ``` **Good Example** (Under limit): ```json { "user_id": "{{user.id}}", "email": "{{user.primary_email_address}}", "role": "{{user.public_metadata.role}}" } ``` ### 2. Reserved Claims These claims are **reserved** and cannot be used in custom templates: | Claim | Reason | |-------|--------| | `azp` | Auto-included default claim | | `exp` | Auto-included default claim | | `iat` | Auto-included default claim | | `iss` | Auto-included default claim | | `jti` | Auto-included default claim | | `nbf` | Auto-included default claim | | `sub` | Auto-included default claim | **Error**: Attempting to override reserved claims returns HTTP 400 with error code `jwt_template_reserved_claim`. ### 3. Session-Bound Claims These claims are **session-specific** and cannot be included in **custom JWTs** (but ARE included in default session tokens): | Claim | Description | |-------|-------------| | `sid` | Session ID | | `v` | Version | | `pla` | Plan | | `fea` | Features | **Why**: Custom JWTs are generated on-demand and not tied to a specific session. **Solution**: If you need session data, use **custom session tokens** instead of custom JWTs. ### 4. Invalid Shortcodes Resolve to `null` ```json { "valid": "{{user.first_name}}", "invalid": "{{user.i_dont_exist}}" } ``` **Result**: ```json { "valid": "John", "invalid": null } ``` **Prevention**: Always test templates with real user data before deploying. ### 5. Custom JWTs May Incur Latency - Default session tokens are cached - Custom JWTs require fetching user data on-demand - This can add **50-200ms** to token generation **When to Use Custom JWTs**: - ✅ Integrating with third-party services (Supabase, Hasura, Grafbase) - ✅ API-to-API authentication - ❌ Not recommended for every frontend request (use default session tokens) ### 6. Organization Data Limitations - Only **active organization** data is included by default - For all organizations, store in `user.public_metadata` and use custom template - `orgs` claim was removed in Core 2 (breaking change) ### 7. Metadata Sync Timing - Metadata changes may take a few seconds to propagate to tokens - Use `user.reload()` in frontend to refresh user object - Backend should always fetch fresh data for critical operations --- ## Official Documentation - **JWT Templates Guide**: https://clerk.com/docs/guides/sessions/jwt-templates - **Custom Session Tokens**: https://clerk.com/docs/backend-requests/making/custom-session-token - **Session Claims Access**: https://clerk.com/docs/upgrade-guides/core-2/node - **Backend SDK**: https://clerk.com/docs/reference/backend/overview - **Supabase Integration**: https://clerk.com/docs/guides/development/integrations/databases/supabase - **Grafbase Integration**: https://clerk.com/docs/guides/development/integrations/databases/grafbase --- ## Quick Reference: All Available Shortcodes ### User Properties ``` {{user.id}} {{user.first_name}} {{user.last_name}} {{user.full_name}} {{user.primary_email_address}} {{user.primary_phone_address}} {{user.image_url}} {{user.created_at}} {{user.username}} {{user.email_verified}} {{user.phone_number_verified}} ``` ### Metadata ``` {{user.public_metadata}} {{user.public_metadata.FIELD_NAME}} {{user.public_metadata.nested.field}} {{user.unsafe_metadata}} {{user.unsafe_metadata.FIELD_NAME}} ``` ### Operators ``` {{shortcode1 || shortcode2}} // Fallback {{shortcode || 'default'}} // Default value "{{shortcode1}} {{shortcode2}}" // String interpolation ``` --- **Last Updated**: 2025-10-22 **Verified Against**: Clerk API v2025-04-10 **Production Tested**: ✅ Multiple frameworks