Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Reference architecture patterns for Clerk authentication. Use when designing application architecture, planning auth flows, or implementing enterprise-grade authentication. Trigger with phrases like "clerk architecture", "clerk design", "clerk system design", "clerk integration patterns".
.claude/skills/jeremylongshore-clerk-reference-architecture/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 46% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 106% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 97% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 71% | 0% |
Reference architectures for implementing Clerk in common application patterns: Next.js full-stack, microservices with shared auth, multi-tenant SaaS, and mobile + web with shared backend.
Browser
│
├─▸ Next.js Middleware (clerkMiddleware)
│ └─▸ Validates session token on every request
│
├─▸ Server Components (auth(), currentUser())
│ └─▸ Direct access to user data, no network call
│
├─▸ Client Components (useUser(), useAuth())
│ └─▸ Real-time auth state via ClerkProvider
│
├─▸ API Routes (auth() for userId, getToken() for JWT)
│ └─▸ Call external services with Clerk JWT
│
└─▸ Webhooks (/api/webhooks/clerk)
└─▸ Sync user data to databasetypescript// app/layout.tsx — entry point import { ClerkProvider } from '@clerk/nextjs' export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <ClerkProvider> <html><body>{children}</body></html> </ClerkProvider> ) }
typescript// middleware.ts — auth boundary import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' const isPublic = createRouteMatcher(['/', '/sign-in(.*)', '/sign-up(.*)', '/api/webhooks(.*)']) export default clerkMiddleware(async (auth, req) => { if (!isPublic(req)) await auth.protect() })
Browser ─▸ API Gateway / BFF (Next.js + Clerk)
│
├─▸ Service A (Node.js) ──── verifies JWT
├─▸ Service B (Python) ──── verifies JWT
└─▸ Service C (Go) ──────── verifies JWTtypescript// BFF: Generate service-specific JWT // app/api/proxy/[service]/route.ts import { auth } from '@clerk/nextjs/server' export async function GET(req: Request, { params }: { params: { service: string } }) { const { userId, getToken } = await auth() if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 }) // Get JWT with service-specific claims const token = await getToken({ template: params.service }) const serviceUrls: Record<string, string> = { billing: process.env.BILLING_SERVICE_URL!, analytics: process.env.ANALYTICS_SERVICE_URL!, notifications: process.env.NOTIFICATION_SERVICE_URL!, } const response = await fetch(`${serviceUrls[params.service]}/api/data`, { headers: { Authorization: `Bearer ${token}` }, }) return Response.json(await response.json()) }
typescript// Downstream service: Verify Clerk JWT // services/billing/src/middleware.ts (Express) import { clerkMiddleware, requireAuth } from '@clerk/express' app.use(clerkMiddleware()) app.get('/api/data', requireAuth(), (req, res) => { // req.auth.userId is available res.json({ userId: req.auth.userId }) })
Tenant A (org_abc) ──┐
Tenant B (org_def) ──┤──▸ Shared App ──▸ Shared DB (tenant-scoped queries)
Tenant C (org_ghi) ──┘typescript// lib/tenant.ts — tenant-scoped data access import { auth } from '@clerk/nextjs/server' export async function getTenantData<T>(query: (orgId: string) => Promise<T>): Promise<T> { const { orgId } = await auth() if (!orgId) throw new Error('No organization selected') return query(orgId) } // Usage: export async function getProjects() { return getTenantData((orgId) => db.project.findMany({ where: { organizationId: orgId } }) ) }
typescript// middleware.ts — enforce org context on tenant routes import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' const isTenantRoute = createRouteMatcher(['/app(.*)']) export default clerkMiddleware(async (auth, req) => { if (isTenantRoute(req)) { const { orgId } = await auth.protect() if (!orgId) { // Redirect to org selector if no org is active return Response.redirect(new URL('/select-org', req.url)) } } })
typescript// app/select-org/page.tsx import { OrganizationSwitcher } from '@clerk/nextjs' export default function SelectOrg() { return ( <div className="flex min-h-screen items-center justify-center"> <div> <h1>Select Your Organization</h1> <OrganizationSwitcher afterSelectOrganizationUrl="/app/dashboard" hidePersonal={true} /> </div> </div> ) }
Web App (Next.js + @clerk/nextjs) ──┐
Mobile App (React Native + @clerk/clerk-expo) ──┤──▸ Backend API (Express + @clerk/express)
└──▸ Databasetypescript// Backend API: Express with Clerk // server.ts import express from 'express' import { clerkMiddleware, requireAuth, getAuth } from '@clerk/express' const app = express() // Apply Clerk middleware globally app.use(clerkMiddleware()) // Public endpoint app.get('/api/public', (req, res) => { res.json({ message: 'Public endpoint' }) }) // Protected endpoint (works with both web and mobile clients) app.get('/api/profile', requireAuth(), async (req, res) => { const { userId } = getAuth(req) const user = await db.user.findUnique({ where: { clerkId: userId } }) res.json({ user }) }) app.listen(3001)
@clerk/express| Pattern | Common Issue | Solution | |---------|-------------|----------| | Full-stack | Middleware redirect loop | Add sign-in route to public routes | | Microservices | JWT template not configured | Create JWT template in Dashboard per service | | Multi-tenant | No org selected | Redirect to org selector before tenant routes | | Mobile + Web | Token not sent from mobile | Include Authorization: Bearer <token> in mobile fetch |
prisma// prisma/schema.prisma model User { id String @id @default(cuid()) clerkId String @unique email String @unique name String? createdAt DateTime @default(now()) posts Post[] orgMemberships OrgMembership[] } model OrgMembership { id String @id @default(cuid()) userId String orgId String // Clerk organization ID role String // org:admin, org:member, etc. user User @relation(fields: [userId], references: [id]) @@unique([userId, orgId]) }
Proceed to clerk-multi-env-setup for multi-environment configuration.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 7,762 | 3,943 | -49% | 1 | 1 | 0% | 1,294 | 2,660 | +106% | 0 | 0 | — |
case-02 | pass→pass | 9,497 | 21,109 | +122% | 1 | 1 | 0% | 1,722 | 3,396 | +97% | 0 | 0 | — |
case-03 | pass→pass | 18,401 | 11,844 | -36% | 1 | 1 | 0% | 2,395 | 4,085 | +71% | 0 | 0 | — |
case-04 | pass→pass | 8,732 | 6,186 | -29% | 1 | 1 | 0% | 1,588 | 3,144 | +98% | 0 | 0 | — |
case-05 | pass→pass | 16,234 | 19,731 | +22% | 1 | 1 | 0% | 2,815 | 3,847 | +37% | 0 | 0 | — |
case-06 | fail→pass | 16,105 | 11,136 | -31% | 1 | 1 | 0% | 2,807 | 3,867 | +38% | 0 | 0 | — |
case-07 | fail→fail | 11,352 | 9,415 | -17% | 1 | 1 | 0% | 2,062 | 3,672 | +78% | 0 | 0 | — |
case-08 | pass→pass | 12,277 | 7,738 | -37% | 1 | 1 | 0% | 2,371 | 3,606 | +52% | 0 | 0 | — |
case-13 | pass→pass | 16,564 | 14,101 | -15% | 1 | 1 | 0% | 3,247 | 4,894 | +51% | 0 | 0 | — |
case-09 | pass→pass | 12,232 | 5,085 | -58% | 1 | 1 | 0% | 2,143 | 3,003 | +40% | 0 | 0 | — |
case-10 | pass→pass | 4,086 | 2,862 | -30% | 1 | 1 | 0% | 713 | 2,518 | +253% | 0 | 0 | — |
case-11 | pass→pass | 9,532 | 4,757 | -50% | 1 | 1 | 0% | 1,749 | 2,927 | +67% | 0 | 0 | — |
case-12 | pass→pass | 5,611 | 5,657 | +1% | 1 | 1 | 0% | 953 | 2,907 | +205% | 0 | 0 | — |
case-14 | pass→pass | 3,154 | 2,596 | -18% | 1 | 1 | 0% | 582 | 2,402 | +313% | 0 | 0 | — |
case-15 | pass→pass | 9,190 | 7,098 | -23% | 1 | 1 | 0% | 1,534 | 3,196 | +108% | 0 | 0 | — |
case-16 | pass→pass | 14,497 | 6,558 | -55% | 1 | 1 | 0% | 2,719 | 3,286 | +21% | 0 | 0 | — |
case-17 | pass→pass | 3,476 | 1,967 | -43% | 1 | 1 | 0% | 587 | 2,230 | +280% | 0 | 0 | — |
case-18 | pass→pass | 6,337 | 4,748 | -25% | 1 | 1 | 0% | 1,258 | 2,865 | +128% | 0 | 0 | — |
case-19 | fail→pass | 9,706 | 1,647 | -83% | 1 | 1 | 0% | 1,529 | 2,227 | +46% | 0 | 0 | — |
case-20 | pass→pass | 11,348 | 9,670 | -15% | 1 | 1 | 0% | 2,090 | 3,621 | +73% | 0 | 0 | — |
case-21 | pass→pass | 11,270 | 6,898 | -39% | 1 | 1 | 0% | 1,894 | 3,257 | +72% | 0 | 0 | — |
case-22 | pass→pass | 4,278 | 2,902 | -32% | 1 | 1 | 0% | 754 | 2,531 | +236% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.