copywriting
Marketing copy for homepages, landing pages, pricing pages, and other conversion-focused web pages.
clerk-core-workflow-b
Implement session management and middleware with Clerk.
Full skill instructions
Implement session management and route protection with Clerk middleware. Covers clerkMiddleware() configuration, auth() patterns, custom session claims, JWT templates for external services, organization-scoped sessions, and session token v2.
@clerk/nextjs installed with ClerkProvider wrapping the appclerk-core-workflow-a completed)// middleware.ts (project root)
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/api/webhooks(.*)',
'/pricing',
'/blog(.*)',
])
const isAdminRoute = createRouteMatcher(['/admin(.*)'])
const isApiRoute = createRouteMatcher(['/api(.*)'])
export default clerkMiddleware(async (auth, req) => {
// Public routes: no auth required
if (isPublicRoute(req)) return
// Admin routes: require org:admin role
if (isAdminRoute(req)) {
await auth.protect({ role: 'org:admin' })
return
}
// All other routes: require authentication
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)(.*)',
],
}
Key behavior: clerkMiddleware() does NOT protect any routes by default. You must explicitly call auth.protect() for routes that require authentication. This is a design decision to avoid over-blocking.
// app/api/data/route.ts
import { auth } from '@clerk/nextjs/server'
export async function GET() {
const { userId, orgId, has } = await auth()
if (!userId) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
// Permission-based authorization
if (!has({ permission: 'org:data:read' })) {
return Response.json({ error: 'Forbidden' }, { status: 403 })
}
const data = orgId
? await db.items.findMany({ where: { organizationId: orgId } })
: await db.items.findMany({ where: { ownerId: userId } })
return Response.json({ data, userId, orgId })
}
export async function POST(req: Request) {
const { userId, orgId, has } = await auth()
if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 })
if (!has({ permission: 'org:data:write' })) {
return Response.json({ error: 'Forbidden' }, { status: 403 })
}
const body = await req.json()
const item = await db.items.create({
data: { ...body, ownerId: userId, organizationId: orgId },
})
return Response.json({ item }, { status: 201 })
}
// app/dashboard/page.tsx
import { auth, currentUser } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const { userId, orgId, orgRole, has, sessionClaims } = await auth()
if (!userId) redirect('/sign-in')
// auth() is free (JWT parsing) — use for lightweight checks
const isAdmin = has({ role: 'org:admin' })
// currentUser() costs a Backend API call — use only when you need full profile
const user = await currentUser()
return (
<div>
<h1>Welcome, {user?.firstName}</h1>
<p>Organization: {orgId || 'Personal account'}</p>
<p>Role: {orgRole || 'N/A'}</p>
{isAdmin && <a href="/admin">Admin Panel</a>}
</div>
)
}
Customize in Dashboard > Sessions > Customize session token:
{
"metadata": "{{user.public_metadata}}",
"email": "{{user.primary_email_address}}"
}
Then declare types and access in code:
// types/clerk.d.ts
declare global {
interface CustomJwtSessionClaims {
metadata?: {
role?: string
plan?: string
}
email?: string
}
}
export {}
// Access custom claims (no API call needed — embedded in JWT)
import { auth } from '@clerk/nextjs/server'
export async function GET() {
const { sessionClaims } = await auth()
const userPlan = sessionClaims?.metadata?.plan || 'free'
const userEmail = sessionClaims?.email
return Response.json({ plan: userPlan, email: userEmail })
}
Warning: Session token cookie limit is 4KB. Custom claims should be under 1.2KB. Store large data in your database, not in session claims.
// app/api/supabase-data/route.ts
import { auth } from '@clerk/nextjs/server'
import { createClient } from '@supabase/supabase-js'
export async function GET() {
const { userId, getToken } = await auth()
if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 })
// Get a JWT with Supabase-compatible claims
// Configure the template in Dashboard > JWT Templates
const supabaseToken = await getToken({ template: 'supabase' })
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ global: { headers: { Authorization: `Bearer ${supabaseToken}` } } }
)
const { data } = await supabase.from('items').select('*')
return Response.json({ data })
}
Configure JWT template in Dashboard > JWT Templates > New template:
{
"sub": "{{user.id}}",
"email": "{{user.primary_email_address}}",
"role": "authenticated",
"aud": "authenticated"
}
'use client'
import { useOrganizationList, useOrganization, useAuth } from '@clerk/nextjs'
export function OrgSwitcher() {
const { organizationList, setActive, isLoaded } = useOrganizationList({
userMemberships: { infinite: true },
})
const { organization } = useOrganization()
if (!isLoaded) return <div>Loading orgs...</div>
return (
<div>
<p>Active: {organization?.name || 'Personal account'}</p>
<ul>
{organizationList?.map(({ organization: org, membership }) => (
<li key={org.id}>
<button onClick={() => setActive({ organization: org.id })}>
{org.name} ({membership.role})
</button>
</li>
))}
<li>
<button onClick={() => setActive({ organization: null })}>
Personal account
</button>
</li>
</ul>
</div>
)
}
'use server'
import { auth } from '@clerk/nextjs/server'
export async function deleteItem(itemId: string) {
const { userId, orgId, has } = await auth()
if (!userId) throw new Error('Unauthorized')
if (!has({ permission: 'org:data:delete' })) {
throw new Error('You do not have permission to delete items')
}
await db.items.delete({ where: { id: itemId, organizationId: orgId } })
return { success: true }
}
export async function updateOrgSettings(settings: Record<string, any>) {
const { orgId, has } = await auth()
if (!orgId) throw new Error('No organization selected')
if (!has({ role: 'org:admin' })) {
throw new Error('Only admins can update organization settings')
}
await db.orgSettings.upsert({
where: { orgId },
update: settings,
create: { orgId, ...settings },
})
return { success: true }
}
| Issue | Cause | Solution |
|---|---|---|
| Middleware redirect loop | Sign-in page not in isPublicRoute | Add /sign-in(.*) to public route matcher |
| 401 on API route | Token not forwarded | Include credentials in fetch or use server-side auth() |
orgId is null | No active organization | Prompt user with <OrganizationSwitcher /> |
has() always false | Permission not assigned to role | Check Dashboard > Organizations > Roles |
| Session expired | Token TTL exceeded | Clerk auto-refreshes; if stuck, clear cookies |
auth() was called but clerkMiddleware() not detected | Middleware missing or wrong location | Ensure middleware.ts at project root |
auth.protect() in middleware returns a 401/redirect before reaching your route handler -- this is more efficient than checking userId in every routehas({ permission: '...' }) over has({ role: '...' }) -- permissions decouple authorization from role namesiss, aud, and exp claims for integrating with Hasura, Supabase, Convex, Neon, and other servicesProceed to clerk-webhooks-events for webhook and event handling.
Marketing copy for homepages, landing pages, pricing pages, and other conversion-focused web pages.
Fast image generation with Flux 2 Klein, optimized for sub-second iteration and multi-reference brand styling.
Psychological principles and mental models for understanding customer behavior and influencing marketing decisions.
Plan searchable and shareable content that drives traffic, builds authority, and generates leads.
Build SEO-optimized pages at scale using templates, data, and proven playbook patterns.
Brainstorm and prioritize marketing strategies tailored to your SaaS stage, budget, and goals.
Optimize content for discovery, citation, and visibility across AI search platforms and LLM assistants.
Systematic improvement of existing marketing copy through seven focused editing passes.
Plan and optimize your website's page hierarchy, navigation, URL structure, and internal linking.
Design and optimize systems connecting marketing, sales, and customer success into a unified revenue engine.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Convert websites into professional HyperFrames videos with guided storyboarding and brand-aligned composition.
Revolutionize Video Creation with AI-Generated Avatars and Voices
Social media on autopilot
Design custom logos instantly with AI.
Text-heavy marketing and design images
Boost Your SEO with Content and SEO Silos
Revolutionize Your Marketing with AI-Powered MarketingBlocks!
10X faster & free AI content creation for 100+ use cases
Tribescaler | Make Your Tweets Go Viral