Clerk Reference Architecture
Overview
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.
Prerequisites
- Understanding of web application architecture
- Familiarity with authentication patterns (JWT, sessions, OAuth)
- Knowledge of your tech stack and scaling requirements
Instructions
Architecture 1: Next.js Full-Stack Application
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 database
// 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>
)
}
// 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()
})
Architecture 2: Microservices with Shared Auth
Browser ─▸ API Gateway / BFF (Next.js + Clerk)
│
├─▸ Service A (Node.js) ──── verifies JWT
├─▸ Service B (Python) ──── verifies JWT
└─▸ Service C (Go) ──────── verifies JWT
// 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())
}
// 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 })
})
Architecture 3: Multi-Tenant SaaS
Tenant A (org_abc) ──┐
Tenant B (org_def) ──┤──▸ Shared App ──▸ Shared DB (tenant-scoped queries)
Tenant C (org_ghi) ──┘
// 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 } })
)
}
// 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))
}
}
})
// 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>
)
}
Architecture 4: Mobile + Web with Shared Backend
Web App (Next.js + @clerk/nextjs) ──┐
Mobile App (React Native + @clerk/clerk-expo) ──┤──▸ Backend API (Express + @clerk/express)
└──▸ Database
// 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)
Output
- Next.js full-stack architecture with middleware, server/client components, and webhooks
- Microservices architecture with BFF proxy and JWT-based service auth
- Multi-tenant SaaS with organization-scoped data access
- Mobile + web with shared Express backend using
@clerk/express
Error Handling
| 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 |
Examples
Database Schema for Clerk Integration
// 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])
}
Resources
Next Steps
Proceed to clerk-multi-env-setup for multi-environment configuration.