Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:03 +08:00
commit d3ec204941
27 changed files with 4067 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
/**
* Cloudflare Worker with Clerk Authentication
*
* This template demonstrates:
* - JWT token verification with @clerk/backend
* - Protected API routes
* - Type-safe Hono context with auth state
* - Proper error handling
*
* Dependencies:
* - @clerk/backend@^2.17.2
* - hono@^4.10.1
*/
import { Hono } from 'hono'
import { verifyToken } from '@clerk/backend'
import { cors } from 'hono/cors'
// Type-safe environment bindings
type Bindings = {
CLERK_SECRET_KEY: string
CLERK_PUBLISHABLE_KEY: string
}
// Context variables with auth state
type Variables = {
userId: string | null
sessionClaims: any | null
}
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
// CORS middleware (adjust origins for production)
app.use('*', cors({
origin: ['http://localhost:5173', 'https://yourdomain.com'],
credentials: true,
}))
/**
* Auth Middleware - Verifies Clerk JWT tokens
*
* CRITICAL SECURITY:
* - Always set authorizedParties to prevent CSRF attacks
* - Use secretKey, not deprecated apiKey
* - Token is in Authorization: Bearer <token> header
*/
app.use('/api/*', async (c, next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
// No auth header - continue as unauthenticated
c.set('userId', null)
c.set('sessionClaims', null)
return next()
}
// Extract token from "Bearer <token>"
const token = authHeader.substring(7)
try {
// Verify token with Clerk
const { data, error } = await verifyToken(token, {
secretKey: c.env.CLERK_SECRET_KEY,
// IMPORTANT: Set to your actual domain(s) to prevent CSRF
// Source: https://clerk.com/docs/reference/backend/verify-token
authorizedParties: [
'http://localhost:5173', // Development
'https://yourdomain.com', // Production
],
})
if (error) {
console.error('[Auth] Token verification failed:', error.message)
c.set('userId', null)
c.set('sessionClaims', null)
} else {
// 'sub' claim contains the user ID
c.set('userId', data.sub)
c.set('sessionClaims', data)
}
} catch (err) {
console.error('[Auth] Token verification error:', err)
c.set('userId', null)
c.set('sessionClaims', null)
}
return next()
})
/**
* Public Routes - No authentication required
*/
app.get('/api/public', (c) => {
return c.json({
message: 'This endpoint is public',
timestamp: new Date().toISOString(),
})
})
app.get('/api/health', (c) => {
return c.json({
status: 'ok',
version: '1.0.0',
})
})
/**
* Protected Routes - Require authentication
*/
app.get('/api/protected', (c) => {
const userId = c.get('userId')
if (!userId) {
return c.json({ error: 'Unauthorized' }, 401)
}
return c.json({
message: 'This endpoint is protected',
userId,
timestamp: new Date().toISOString(),
})
})
app.get('/api/user/profile', (c) => {
const userId = c.get('userId')
const sessionClaims = c.get('sessionClaims')
if (!userId) {
return c.json({ error: 'Unauthorized' }, 401)
}
// Access custom claims from JWT template (if configured)
return c.json({
userId,
email: sessionClaims?.email,
role: sessionClaims?.role,
organizationId: sessionClaims?.organization_id,
})
})
/**
* POST Example - Create resource with auth
*/
app.post('/api/items', async (c) => {
const userId = c.get('userId')
if (!userId) {
return c.json({ error: 'Unauthorized' }, 401)
}
const body = await c.req.json()
// Validate and process body
// Example: save to D1, KV, or R2
return c.json({
success: true,
itemId: crypto.randomUUID(),
userId,
}, 201)
})
/**
* Role-Based Access Control Example
*/
app.get('/api/admin/dashboard', (c) => {
const userId = c.get('userId')
const sessionClaims = c.get('sessionClaims')
if (!userId) {
return c.json({ error: 'Unauthorized' }, 401)
}
// Check role from custom JWT claims
const role = sessionClaims?.role
if (role !== 'admin') {
return c.json({ error: 'Forbidden: Admin access required' }, 403)
}
return c.json({
message: 'Admin dashboard data',
userId,
})
})
/**
* Error Handling
*/
app.onError((err, c) => {
console.error('[Error]', err)
return c.json({ error: 'Internal Server Error' }, 500)
})
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404)
})
/**
* Export the Hono app
*
* ES Module format for Cloudflare Workers
*/
export default app

View File

@@ -0,0 +1,46 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-clerk-worker",
"main": "src/index.ts",
"account_id": "YOUR_ACCOUNT_ID",
"compatibility_date": "2025-10-11",
"observability": {
"enabled": true
},
"vars": {
"CLERK_PUBLISHABLE_KEY": "pk_test_..."
}
/**
* CRITICAL: Never commit CLERK_SECRET_KEY to version control
*
* To set CLERK_SECRET_KEY:
*
* 1. Production:
* wrangler secret put CLERK_SECRET_KEY
*
* 2. Local development:
* Create .dev.vars file (see templates/env-examples/.dev.vars.example)
*
* After setting, access via c.env.CLERK_SECRET_KEY in your Worker
*/
/**
* Optional: Add other Cloudflare bindings
*
* KV Namespace:
* "kv_namespaces": [
* { "binding": "AUTH_CACHE", "id": "YOUR_KV_ID" }
* ]
*
* D1 Database:
* "d1_databases": [
* { "binding": "DB", "database_name": "my-db", "database_id": "YOUR_DB_ID" }
* ]
*
* R2 Bucket:
* "r2_buckets": [
* { "binding": "ASSETS", "bucket_name": "my-bucket" }
* ]
*/
}

View File

@@ -0,0 +1,63 @@
# Clerk Environment Variables for Cloudflare Workers
#
# Copy this file to .dev.vars for local development
# Get your keys from https://dashboard.clerk.com
# ==========================================
# LOCAL DEVELOPMENT (.dev.vars)
# ==========================================
# Secret Key (server-side verification)
CLERK_SECRET_KEY=sk_test_...
# Publishable Key (can be in wrangler.jsonc or here)
CLERK_PUBLISHABLE_KEY=pk_test_...
# ==========================================
# PRODUCTION DEPLOYMENT
# ==========================================
# For production, use wrangler secrets:
#
# 1. Set secret key (encrypted, not in wrangler.jsonc):
# wrangler secret put CLERK_SECRET_KEY
#
# 2. Set publishable key in wrangler.jsonc:
# {
# "vars": {
# "CLERK_PUBLISHABLE_KEY": "pk_live_..."
# }
# }
# ==========================================
# SECURITY NOTES
# ==========================================
# 1. NEVER commit .dev.vars to version control
# .dev.vars is in .gitignore by default
#
# 2. Use different keys for development and production
# - Development: pk_test_... / sk_test_...
# - Production: pk_live_... / sk_live_...
#
# 3. Production secrets via wrangler secret put
# This encrypts secrets, they won't appear in wrangler.jsonc
#
# 4. Rotate CLERK_SECRET_KEY if compromised
# Generate new keys in Clerk Dashboard
# ==========================================
# OPTIONAL - Additional Bindings
# ==========================================
# If your Worker uses other services, add them here:
# DATABASE_URL=...
# API_KEY=...
# ==========================================
# REFERENCE
# ==========================================
# Official Docs:
# https://clerk.com/docs/reference/backend/verify-token
# https://developers.cloudflare.com/workers/wrangler/configuration/

View File

@@ -0,0 +1,89 @@
# Clerk Environment Variables for Next.js
#
# Copy this file to .env.local and fill in your actual values
# Get your keys from https://dashboard.clerk.com
# ==========================================
# REQUIRED
# ==========================================
# Publishable Key (safe to expose to client)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
# Secret Key (NEVER expose to client, server-side only)
CLERK_SECRET_KEY=sk_test_...
# ==========================================
# OPTIONAL - Custom Pages
# ==========================================
# Uncomment to use custom sign-in/sign-up pages
# NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
# NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
# ==========================================
# OPTIONAL - Redirect URLs
# ==========================================
# Where to redirect after sign-in (forced - always goes here)
# NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
# Where to redirect after sign-up (forced - always goes here)
# NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding
# Fallback redirect if no forced redirect is set (default: /)
# NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
# NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/
# ==========================================
# OPTIONAL - Webhooks
# ==========================================
# Webhook signing secret for verifying Clerk webhooks
# Get this from Clerk Dashboard > Webhooks > Add Endpoint
# CLERK_WEBHOOK_SIGNING_SECRET=whsec_...
# ==========================================
# OPTIONAL - Multi-Domain (Satellite Domains)
# ==========================================
# For multi-domain authentication
# NEXT_PUBLIC_CLERK_DOMAIN=accounts.yourdomain.com
# NEXT_PUBLIC_CLERK_IS_SATELLITE=true
# ==========================================
# OPTIONAL - Advanced Configuration
# ==========================================
# Custom Clerk JS URL (usually not needed)
# NEXT_PUBLIC_CLERK_JS_URL=https://...
# Proxy URL for requests (enterprise feature)
# NEXT_PUBLIC_CLERK_PROXY_URL=https://...
# Disable telemetry
# CLERK_TELEMETRY_DISABLED=1
# ==========================================
# SECURITY NOTES
# ==========================================
# 1. NEVER commit .env.local to version control
# Add .env.local to .gitignore
#
# 2. Use different keys for development and production
# - Development: pk_test_... / sk_test_...
# - Production: pk_live_... / sk_live_...
#
# 3. NEVER use NEXT_PUBLIC_ prefix for secrets
# NEXT_PUBLIC_ variables are exposed to the browser
#
# 4. Rotate CLERK_SECRET_KEY if compromised
# Generate new keys in Clerk Dashboard
# ==========================================
# REFERENCE
# ==========================================
# Official Docs:
# https://clerk.com/docs/guides/development/clerk-environment-variables

View File

@@ -0,0 +1,46 @@
# Clerk Environment Variables for React + Vite
#
# Copy this file to .env.local and fill in your actual values
# Get your keys from https://dashboard.clerk.com
# ==========================================
# REQUIRED
# ==========================================
# Publishable Key (safe to expose to client)
# CRITICAL: Must use VITE_ prefix for Vite to expose to client
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
# ==========================================
# SECURITY NOTES
# ==========================================
# 1. NEVER commit .env.local to version control
# Add .env.local to .gitignore
#
# 2. Must use VITE_ prefix for client-side variables
# Without VITE_ prefix, variable won't be available
#
# 3. Only VITE_ prefixed vars are exposed to browser
# Never use VITE_ prefix for secrets
#
# 4. Restart dev server after changing .env.local
# Vite only reads env vars on startup
#
# 5. Use different keys for development and production
# - Development: pk_test_...
# - Production: pk_live_...
# ==========================================
# ACCESS IN CODE
# ==========================================
# Use import.meta.env to access:
# const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
# ==========================================
# REFERENCE
# ==========================================
# Vite Env Vars: https://vitejs.dev/guide/env-and-mode.html
# Clerk Docs: https://clerk.com/docs/references/react/clerk-provider

View File

@@ -0,0 +1,23 @@
{
"$comment": "Advanced Clerk JWT Template - Multi-Tenant with Fallbacks",
"$description": "This template demonstrates advanced features: string interpolation, conditional expressions, nested metadata access, and organization claims. Copy this JSON (without $ prefixed fields) into Clerk Dashboard.",
"user_id": "{{user.id}}",
"email": "{{user.primary_email_address}}",
"full_name": "{{user.last_name}} {{user.first_name}}",
"avatar": "{{user.image_url}}",
"role": "{{user.public_metadata.role || 'user'}}",
"department": "{{user.public_metadata.department || 'general'}}",
"permissions": "{{user.public_metadata.permissions}}",
"org_id": "{{user.public_metadata.org_id}}",
"org_slug": "{{user.public_metadata.org_slug}}",
"org_role": "{{user.public_metadata.org_role}}",
"interests": "{{user.public_metadata.profile.interests}}",
"has_verified_contact": "{{user.email_verified || user.phone_number_verified}}",
"age": "{{user.public_metadata.age || user.unsafe_metadata.age || 18}}",
"onboarding_complete": "{{user.public_metadata.onboardingComplete || false}}"
}

View File

@@ -0,0 +1,8 @@
{
"$comment": "Basic Clerk JWT Template - Role-Based Access Control",
"$description": "This template includes minimal user information for role-based authentication. Copy this JSON (without comments) into Clerk Dashboard > Sessions > Customize session token > Create template.",
"user_id": "{{user.id}}",
"email": "{{user.primary_email_address}}",
"role": "{{user.public_metadata.role || 'user'}}"
}

View File

@@ -0,0 +1,14 @@
{
"$comment": "Grafbase GraphQL Integration JWT Template",
"$description": "This template is for Grafbase integration with role-based access control. Grafbase uses 'groups' array for authorization. Name it 'grafbase' in Clerk Dashboard.",
"$usage": "const token = await getToken({ template: 'grafbase' }); // Use in GraphQL requests",
"$grafbase_config": "In grafbase.toml: [auth.providers.clerk] issuer = 'https://your-app.clerk.accounts.dev' jwks = 'https://your-app.clerk.accounts.dev/.well-known/jwks.json'",
"sub": "{{user.id}}",
"groups": [
"org:{{user.public_metadata.org_role || 'member'}}",
"user:authenticated"
],
"email": "{{user.primary_email_address}}",
"name": "{{user.full_name || user.first_name}}"
}

View File

@@ -0,0 +1,17 @@
{
"$comment": "Supabase Integration JWT Template",
"$description": "This template is designed for Supabase integration. Name it 'supabase' in Clerk Dashboard. Use with getToken({ template: 'supabase' }) to authenticate Supabase client.",
"$usage": "const token = await getToken({ template: 'supabase' }); const supabase = createClient(url, key, { global: { headers: { Authorization: `Bearer ${token}` } } });",
"aud": "authenticated",
"email": "{{user.primary_email_address}}",
"app_metadata": {
"provider": "clerk",
"providers": ["clerk"]
},
"user_metadata": {
"full_name": "{{user.full_name || user.first_name || 'User'}}",
"avatar_url": "{{user.image_url}}",
"email": "{{user.primary_email_address}}"
}
}

View File

@@ -0,0 +1,101 @@
/**
* Next.js App Router Layout with Clerk
*
* Place this in app/layout.tsx
*
* Dependencies:
* - @clerk/nextjs@^6.33.3
*/
import { ClerkProvider } from '@clerk/nextjs'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'My App',
description: 'Authenticated with Clerk',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider>
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
</ClerkProvider>
)
}
/**
* With Dark Mode Support (using next-themes):
*
* 1. Install: npm install next-themes
* 2. Use this pattern:
*/
/*
import { ClerkProvider } from '@clerk/nextjs'
import { ThemeProvider } from 'next-themes'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider>
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
</ClerkProvider>
)
}
*/
/**
* With Clerk Appearance Customization:
*/
/*
import { ClerkProvider } from '@clerk/nextjs'
import { dark } from '@clerk/themes'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider
appearance={{
baseTheme: dark,
variables: {
colorPrimary: '#3b82f6',
colorBackground: '#0f172a',
},
elements: {
formButtonPrimary: 'bg-blue-500 hover:bg-blue-600',
card: 'shadow-xl',
},
}}
>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
)
}
*/

View File

@@ -0,0 +1,143 @@
/**
* Next.js Middleware with Clerk Authentication
*
* This middleware protects routes using Clerk's clerkMiddleware.
* Place this file in the root of your Next.js project.
*
* Dependencies:
* - @clerk/nextjs@^6.33.3
*
* CRITICAL (v6 Breaking Change):
* - auth.protect() is now async - must use await
* - Source: https://clerk.com/changelog/2024-10-22-clerk-nextjs-v6
*/
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
/**
* Define public routes (routes that don't require authentication)
*
* Glob patterns supported:
* - '/path' - exact match
* - '/path(.*)' - path and all sub-paths
* - '/api/public/*' - wildcard
*/
const isPublicRoute = createRouteMatcher([
'/', // Homepage
'/sign-in(.*)', // Sign-in page and sub-paths
'/sign-up(.*)', // Sign-up page and sub-paths
'/api/public(.*)', // Public API routes
'/api/webhooks(.*)', // Webhook endpoints
'/about', // Static pages
'/pricing',
'/contact',
])
/**
* Alternative: Define protected routes instead
*
* Uncomment this pattern if you prefer to explicitly protect
* specific routes rather than inverting the logic:
*/
/*
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/profile(.*)',
'/admin(.*)',
'/api/private(.*)',
])
export default clerkMiddleware(async (auth, request) => {
if (isProtectedRoute(request)) {
await auth.protect()
}
})
*/
/**
* Default Pattern: Protect all routes except public ones
*
* CRITICAL:
* - auth.protect() MUST be awaited (async in v6)
* - Without await, route protection will not work
*/
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect()
}
})
/**
* Matcher Configuration
*
* Defines which paths run middleware.
* This is the recommended configuration from Clerk.
*/
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)(.*)',
],
}
/**
* Advanced: Role-Based Protection
*
* Protect routes based on user role or organization membership:
*/
/*
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isAdminRoute = createRouteMatcher(['/admin(.*)'])
const isOrgRoute = createRouteMatcher(['/org(.*)'])
export default clerkMiddleware(async (auth, request) => {
// Admin routes require 'admin' role
if (isAdminRoute(request)) {
await auth.protect((has) => {
return has({ role: 'admin' })
})
}
// Organization routes require organization membership
if (isOrgRoute(request)) {
await auth.protect((has) => {
return has({ permission: 'org:member' })
})
}
// All other routes use default protection
if (!isPublicRoute(request)) {
await auth.protect()
}
})
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)(.*)',
],
}
*/
/**
* Troubleshooting:
*
* 1. Routes not protected?
* - Ensure auth.protect() is awaited
* - Check matcher configuration includes your routes
* - Verify middleware.ts is in project root
*
* 2. Infinite redirects?
* - Ensure sign-in/sign-up routes are in isPublicRoute
* - Check NEXT_PUBLIC_CLERK_SIGN_IN_URL in .env.local
*
* 3. API routes returning HTML?
* - Verify '/(api|trpc)(.*)' is in matcher
* - Check API routes are not in isPublicRoute if protected
*
* Official Docs: https://clerk.com/docs/reference/nextjs/clerk-middleware
*/

View File

@@ -0,0 +1,114 @@
/**
* Server Component with Clerk Auth
*
* Demonstrates using auth() and currentUser() in Server Components
*
* CRITICAL (v6): auth() is now async - must use await
*/
import { auth, currentUser } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
/**
* Option 1: Lightweight auth check
*
* Use auth() when you only need userId/sessionId
* This is faster than currentUser()
*/
const { userId, sessionId } = await auth()
// Redirect if not authenticated (shouldn't happen if middleware configured)
if (!userId) {
redirect('/sign-in')
}
/**
* Option 2: Full user object
*
* Use currentUser() when you need full user data
* Heavier than auth(), so use sparingly
*/
const user = await currentUser()
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold">Dashboard</h1>
<div className="mt-4 space-y-2">
<p>
<strong>User ID:</strong> {userId}
</p>
<p>
<strong>Session ID:</strong> {sessionId}
</p>
<p>
<strong>Email:</strong>{' '}
{user?.primaryEmailAddress?.emailAddress}
</p>
<p>
<strong>Name:</strong> {user?.firstName} {user?.lastName}
</p>
{/* Access public metadata */}
{user?.publicMetadata && (
<div>
<strong>Role:</strong>{' '}
{(user.publicMetadata as any).role || 'user'}
</div>
)}
</div>
</div>
)
}
/**
* API Route Example (app/api/user/route.ts)
*/
/*
import { auth, currentUser } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function GET() {
const { userId } = await auth()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const user = await currentUser()
return NextResponse.json({
userId,
email: user?.primaryEmailAddress?.emailAddress,
name: `${user?.firstName} ${user?.lastName}`,
})
}
*/
/**
* Protected API Route with POST (app/api/items/route.ts)
*/
/*
import { auth } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const { userId } = await auth()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
// Validate and process
// Example: save to database with userId
return NextResponse.json({
success: true,
itemId: crypto.randomUUID(),
userId,
}, { status: 201 })
}
*/

215
templates/react/App.tsx Normal file
View File

@@ -0,0 +1,215 @@
/**
* React App Component with Clerk Hooks
*
* Demonstrates:
* - useUser() for user data
* - useAuth() for session tokens
* - useClerk() for auth methods
* - Proper loading state handling
*/
import { useUser, useAuth, useClerk, SignInButton, UserButton } 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, getToken } = useAuth()
// Get Clerk instance for advanced operations
const { openSignIn, signOut } = useClerk()
/**
* CRITICAL: Always check isLoaded before rendering
*
* Why: Prevents flash of wrong content while Clerk initializes
* Source: https://clerk.com/docs/references/react/use-user
*/
if (!isLoaded) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Loading...</div>
</div>
)
}
/**
* Unauthenticated View
*/
if (!isSignedIn) {
return (
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
<h1 className="text-4xl font-bold">Welcome</h1>
<p className="text-gray-600">Sign in to continue</p>
{/* Option 1: Clerk's pre-built button */}
<SignInButton mode="modal">
<button className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
Sign In
</button>
</SignInButton>
{/* Option 2: Custom button with openSignIn() */}
{/* <button
onClick={() => openSignIn()}
className="px-6 py-2 bg-blue-500 text-white rounded-lg"
>
Sign In
</button> */}
</div>
)
}
/**
* Authenticated View
*/
return (
<div className="container mx-auto p-8">
<header className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold">Dashboard</h1>
{/* Clerk's pre-built user button (profile + sign out) */}
<UserButton afterSignOutUrl="/" />
</header>
<div className="space-y-4">
<div className="p-6 bg-white rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">User Information</h2>
<dl className="space-y-2">
<div>
<dt className="font-medium text-gray-700">User ID</dt>
<dd className="text-gray-900">{userId}</dd>
</div>
<div>
<dt className="font-medium text-gray-700">Email</dt>
<dd className="text-gray-900">
{user.primaryEmailAddress?.emailAddress}
</dd>
</div>
<div>
<dt className="font-medium text-gray-700">Name</dt>
<dd className="text-gray-900">
{user.firstName} {user.lastName}
</dd>
</div>
{/* Access public metadata */}
{user.publicMetadata && Object.keys(user.publicMetadata).length > 0 && (
<div>
<dt className="font-medium text-gray-700">Metadata</dt>
<dd className="text-gray-900">
<pre className="text-sm bg-gray-100 p-2 rounded">
{JSON.stringify(user.publicMetadata, null, 2)}
</pre>
</dd>
</div>
)}
</dl>
</div>
{/* Example: Call protected API */}
<ProtectedAPIExample getToken={getToken} />
{/* Custom sign out button */}
<button
onClick={() => signOut()}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
Sign Out
</button>
</div>
</div>
)
}
/**
* Example Component: Calling Protected API
*/
function ProtectedAPIExample({ getToken }: { getToken: () => Promise<string | null> }) {
const [data, setData] = React.useState<any>(null)
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState<string | null>(null)
const fetchProtectedData = async () => {
setLoading(true)
setError(null)
try {
// Get fresh session token (auto-refreshes)
const token = await getToken()
if (!token) {
throw new Error('No session token available')
}
// Call your API with Authorization header
const response = await fetch('https://your-worker.workers.dev/api/protected', {
headers: {
'Authorization': `Bearer ${token}`,
},
})
if (!response.ok) {
throw new Error(`API error: ${response.status}`)
}
const result = await response.json()
setData(result)
} catch (err: any) {
setError(err.message)
} finally {
setLoading(false)
}
}
return (
<div className="p-6 bg-white rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">Protected API Call</h2>
<button
onClick={fetchProtectedData}
disabled={loading}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
{loading ? 'Loading...' : 'Fetch Protected Data'}
</button>
{error && (
<div className="mt-4 p-4 bg-red-50 text-red-700 rounded">
Error: {error}
</div>
)}
{data && (
<div className="mt-4">
<pre className="text-sm bg-gray-100 p-4 rounded overflow-auto">
{JSON.stringify(data, null, 2)}
</pre>
</div>
)}
</div>
)
}
export default App
/**
* Troubleshooting:
*
* 1. "Missing Publishable Key" error?
* - Check .env.local has VITE_CLERK_PUBLISHABLE_KEY
* - Restart dev server after adding env var
*
* 2. Flash of unauthenticated content?
* - Always check isLoaded before rendering
* - Show loading state while isLoaded is false
*
* 3. Token not working with API?
* - Ensure getToken() is called fresh (don't cache)
* - Check Authorization header format: "Bearer <token>"
* - Verify API is using @clerk/backend to verify token
*/

66
templates/react/main.tsx Normal file
View File

@@ -0,0 +1,66 @@
/**
* React + Vite Entry Point with Clerk
*
* Place this in src/main.tsx
*
* Dependencies:
* - @clerk/clerk-react@^5.51.0
*/
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
// CRITICAL: Must use VITE_ prefix for Vite to expose to client
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
if (!PUBLISHABLE_KEY) {
throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY in .env.local')
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ClerkProvider publishableKey={PUBLISHABLE_KEY}>
<App />
</ClerkProvider>
</React.StrictMode>,
)
/**
* With Dark Mode Support (using custom theme):
*/
/*
import { ClerkProvider } from '@clerk/clerk-react'
import { dark } from '@clerk/themes'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
appearance={{
baseTheme: dark,
variables: {
colorPrimary: '#3b82f6',
},
}}
>
<App />
</ClerkProvider>
</React.StrictMode>,
)
*/
/**
* Environment Variables:
*
* Create .env.local with:
* VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
*
* CRITICAL:
* - Must use VITE_ prefix (Vite requirement)
* - Never commit .env.local to version control
* - Use different keys for development and production
*/

View File

@@ -0,0 +1,125 @@
/**
* Custom JWT Session Claims Type Definitions
*
* This file provides TypeScript type safety for custom JWT claims in Clerk.
* Place this in your project's types/ directory (e.g., types/globals.d.ts).
*
* After adding this, sessionClaims will have auto-complete and type checking
* for your custom claims.
*
* Usage:
* ```typescript
* import { auth } from '@clerk/nextjs/server'
*
* const { sessionClaims } = await auth()
* const role = sessionClaims?.metadata?.role // Type: 'admin' | 'moderator' | 'user' | undefined
* ```
*/
export {}
declare global {
/**
* Extend Clerk's CustomJwtSessionClaims interface with your custom claims.
*
* IMPORTANT: The structure must match your JWT template exactly.
*/
interface CustomJwtSessionClaims {
/**
* Custom metadata claims
*/
metadata: {
/**
* User's role in the application
* Maps to: {{user.public_metadata.role}}
*/
role?: 'admin' | 'moderator' | 'user'
/**
* Whether user has completed onboarding
* Maps to: {{user.public_metadata.onboardingComplete}}
*/
onboardingComplete?: boolean
/**
* User's department
* Maps to: {{user.public_metadata.department}}
*/
department?: string
/**
* User's permissions array
* Maps to: {{user.public_metadata.permissions}}
*/
permissions?: string[]
/**
* Organization ID for multi-tenant apps
* Maps to: {{user.public_metadata.org_id}}
*/
organizationId?: string
/**
* Organization slug for multi-tenant apps
* Maps to: {{user.public_metadata.org_slug}}
*/
organizationSlug?: string
/**
* User's role in organization
* Maps to: {{user.public_metadata.org_role}}
*/
organizationRole?: string
}
/**
* User's email address (if included in template)
* Maps to: {{user.primary_email_address}}
*/
email?: string
/**
* User's full name (if included in template)
* Maps to: {{user.full_name}}
*/
full_name?: string
/**
* User ID (if included in template)
* Maps to: {{user.id}}
* Note: Also available as 'sub' in default claims
*/
user_id?: string
}
}
/**
* Example Usage in Next.js Server Component:
*
* ```typescript
* import { auth } from '@clerk/nextjs/server'
*
* export default async function AdminPage() {
* const { sessionClaims } = await auth()
*
* // TypeScript knows about these properties now
* if (sessionClaims?.metadata?.role !== 'admin') {
* return <div>Unauthorized</div>
* }
*
* return <div>Admin Dashboard</div>
* }
* ```
*
* Example Usage in Cloudflare Workers:
*
* ```typescript
* import { verifyToken } from '@clerk/backend'
*
* const { data } = await verifyToken(token, { secretKey })
*
* // Access custom claims with type safety
* const role = data.metadata?.role
* const isAdmin = role === 'admin'
* ```
*/

View File

@@ -0,0 +1,42 @@
{
"$comment": "Vite + Clerk: package.json with increased header size limit",
"$description": "This template shows how to configure Vite dev server to handle Clerk's authentication handshake tokens, which can exceed the default 8KB Node.js header limit when using custom JWT claims.",
"name": "vite-clerk-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "NODE_OPTIONS='--max-http-header-size=32768' vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@clerk/clerk-react": "^5.51.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.0"
},
"$notes": [
"The key change is in the 'dev' script:",
"NODE_OPTIONS='--max-http-header-size=32768' increases the limit from 8KB to 32KB",
"",
"For Windows PowerShell, use:",
"\"dev\": \"cross-env NODE_OPTIONS=--max-http-header-size=32768 vite\"",
"And install: npm install -D cross-env",
"",
"This prevents '431 Request Header Fields Too Large' errors when:",
"- Testing Clerk authentication in development mode",
"- Using custom JWT claims that increase token size",
"- Clerk's __clerk_handshake parameter exceeds header limit",
"",
"Production deployments (Cloudflare, Vercel, Netlify) don't need this fix."
]
}