17 KiB
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
- Default Claims (Auto-Included)
- User Property Shortcodes
- Organization Claims
- Metadata Access
- Advanced Template Features
- Creating JWT Templates
- TypeScript Type Safety
- Common Use Cases
- Limitations & Gotchas
- Official Documentation
Default Claims (Auto-Included)
Every JWT generated by Clerk automatically includes these claims. These cannot be overridden in custom templates.
{
"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
{
"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
{
"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
{
"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:
orgsclaim (previously contained all user organizations)
Accessing Organization Data in Templates
{
"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:
- Store org data in
user.public_metadatavia Clerk API - 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
{
"all_public": "{{user.public_metadata}}",
"all_unsafe": "{{user.unsafe_metadata}}"
}
Result:
{
"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:
{
"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:
{
"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:
{
"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:
{
"full_name": "{{user.last_name}} {{user.first_name}}",
"greeting": "Hello, {{user.first_name}}!",
"email_with_name": "{{user.full_name}} <{{user.primary_email_address}}>"
}
Result:
{
"full_name": "Doe John",
"greeting": "Hello, John!",
"email_with_name": "John Doe <john@example.com>"
}
IMPORTANT: Interpolated values are always strings, even if null values are present.
Conditional Expressions (Fallbacks)
Use || operator to provide fallback values:
{
"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:
{
"age": "{{user.public_metadata.age || user.unsafe_metadata.age || 18}}"
}
This checks:
user.public_metadata.age(if null/false, continue)user.unsafe_metadata.age(if null/false, continue)18(default value)
Boolean Checks
{
"has_verified_contact": "{{user.email_verified || user.phone_number_verified}}",
"is_complete": "{{user.public_metadata.profileComplete || false}}"
}
Result:
{
"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
- Go to Sessions page in Clerk Dashboard
- Click Customize session token
- Choose Create template or use pre-built templates
Step 2: Configure Template Properties
Template Properties:
{
"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:
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:
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:
export {}
declare global {
interface CustomJwtSessionClaims {
metadata: {
role?: 'admin' | 'moderator' | 'user'
onboardingComplete?: boolean
department?: string
organizationId?: string
}
}
}
Usage:
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:
{
"email": "{{user.primary_email_address}}",
"role": "{{user.public_metadata.role || 'user'}}",
"permissions": "{{user.public_metadata.permissions}}"
}
Backend Verification:
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:
{
"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:
// 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):
{
"email": "{{user.primary_email_address}}",
"app_metadata": {
"provider": "clerk"
},
"user_metadata": {
"full_name": "{{user.full_name}}",
"avatar_url": "{{user.image_url}}"
}
}
Usage:
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):
{
"sub": "{{user.id}}",
"groups": ["org:admin", "org:member"]
}
Grafbase Configuration:
[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):
{
"email": "{{user.primary_email_address}}",
"bio": "{{user.public_metadata.bio}}", // 6KB bio field
"all_metadata": "{{user.public_metadata}}" // Entire object
}
Good Example (Under limit):
{
"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
{
"valid": "{{user.first_name}}",
"invalid": "{{user.i_dont_exist}}"
}
Result:
{
"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_metadataand use custom template orgsclaim 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