Kit uses Clerk for authentication — a hosted identity platform that handles sign-up, sign-in, social logins, multi-factor authentication, and user management. Clerk is deeply integrated into the boilerplate, from middleware-based route protection to automatic database synchronization via webhooks.
This page explains the architecture. If you want to set up Clerk in your project, head to Setup & Providers.
Kit works without Clerk during development. Run
pnpm dev:no-clerk to start with a mock authentication system. You can explore the entire dashboard without creating a Clerk account first.How Authentication Works
The authentication system connects four layers that work together to protect your application:
- Middleware intercepts every request and checks whether the route is public or protected. Protected routes require a valid Clerk session.
- ClerkProvider wraps your application and makes auth state available to all components — both server and client.
- Auth Hooks and Helpers let you access the current user in Server Components (
getServerAuth()) and Client Components (useConditionalAuth()). - Webhooks keep your database in sync with Clerk. When a user signs up, Clerk fires a webhook that creates the corresponding database record with a Free Tier and initial credits.
Request Flow:
Browser Request
|
v
Middleware (middleware.ts)
|--- Public route? ---> Allow through (auth state still available)
|--- Protected route? --> Clerk auth.protect() --> Redirect to /login if unauthenticated
|
v
ClerkProvider (layout.tsx)
|--- Production: Real Clerk SDK
|--- Test/Demo: Bypassed (no ClerkProvider)
|
v
Route Component
|--- Server Component: getServerAuth() / currentUser()
|--- Client Component: useConditionalAuth() / useConditionalUser()
|
v
Database (via webhook sync)
|--- user.created --> prisma.user.upsert (Free Tier + credits)
|--- user.updated --> prisma.user.update (email, name)
|--- user.deleted --> prisma.user.delete
ClerkProvider Integration
The
ClerkProvider wraps the entire application in the root layout. It conditionally renders based on whether Clerk should be active in the current environment:src/app/layout.tsx
// Production/Development: Wrap with ClerkProvider
// Test: Return content directly without ClerkProvider
return useClerk ? (
<ClerkProvider
dynamic
appearance={{
elements: {
formButtonPrimary: 'bg-primary hover:bg-primary/90',
footerActionLink: 'text-primary hover:text-primary/90',
},
}}
>
{content}
</ClerkProvider>
) : (
content
)
}
Two things to note here:
- The
dynamicprop prevents hydration errors with Next.js streaming. Without it, the Clerk session token can differ between server and client rendering, causing a mismatch. - In test and demo mode, the
ClerkProvideris omitted entirely. Components receive mock auth data instead, and the Clerk SDK is never bundled.
Hash Routing
Kit uses Clerk's hash-based routing for authentication pages:
| Route | Purpose | Clerk Routing |
|---|---|---|
/login | Sign-in page | routing="hash" renders <SignIn /> |
/register | Sign-up page | routing="hash" renders <SignUp /> |
Hash routing (
/login#/factor-one, /register#/verify-email) keeps multi-step auth flows on a single Next.js page without requiring additional route handlers. Clerk manages the step transitions internally using URL hash fragments.The auth URLs are configured via environment variables:
bash
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/login
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/register
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
Pre-Configured Auth Flows
Kit ships with three authentication flows ready to use:
Sign-Up — New users register at
/register. Clerk handles email verification, password requirements, and optional social login. After successful registration, users are redirected to /dashboard. A Clerk webhook fires user.created, which initializes the database record with a Free Tier.Sign-In — Returning users sign in at
/login. Clerk supports email/password, social login (Google, GitHub), and multi-factor authentication. After sign-in, users land on /dashboard.Password Reset — Built into Clerk's sign-in flow. Users click "Forgot password?" on the login page and receive a reset email. No custom implementation needed.
The register page supports checkout integration. If a user arrives at
/register?tier=pro&billing=monthly, they are automatically redirected to the payment checkout after completing registration.Environment-Aware Architecture
Kit's auth system operates in three modes, determined automatically by environment variables. This means you never need to change application code when switching between development, testing, and production.
| Mode | When Active | Behavior |
|---|---|---|
| Production | VERCEL_ENV=production | Real Clerk authentication. Cannot be overridden. |
| Test / CI | NODE_ENV=test or CLERK_ENABLED=false | Mock auth with test user. Clerk SDK is not loaded. |
| Demo | NEXT_PUBLIC_DEMO_MODE=true | Demo login form with tier switching. No real auth. |
| Development | Default | Uses Clerk if API keys are present, mock auth otherwise. |
The shouldUseClerk() Function
All environment detection is centralized in a single function. Every part of the application — middleware, layouts, components, API routes — calls
shouldUseClerk() instead of checking environment variables directly:src/lib/auth/config.ts
export function shouldUseClerk(): boolean {
// Priority 0: Demo Mode always bypasses Clerk (even in production)
// This allows public demo deployments without real auth
if (process.env.NEXT_PUBLIC_DEMO_MODE === 'true') {
return false
}
// Priority 1: Never use test auth in production (safety first)
if (isProduction()) {
return true
}
// Priority 2: Use test auth in test environments (explicit disable)
if (isTestEnvironment()) {
return false
}
// Priority 3: In development, use Clerk if credentials are available
return hasClerkCredentials()
}
Decision Priority
The function evaluates conditions in a strict order:
- Demo Mode (priority 0) — Always bypasses Clerk, even in production. This enables public demo deployments without real authentication.
- Production (priority 1) — Always uses Clerk. This is a safety measure — production can never accidentally run in test mode.
- Test Environment (priority 2) — Never uses Clerk. Detected via
NEXT_PUBLIC_CLERK_ENABLED=falseorNODE_ENV=test. - Development (priority 3) — Uses Clerk only if
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYis set. If missing, falls back to mock auth gracefully.
For debugging, the
authConfig object provides a snapshot of the current configuration:src/lib/auth/config.ts
export const authConfig = {
isTest: isTestEnvironment(),
isProd: isProduction(),
hasKeys: hasClerkCredentials(),
useClerk: shouldUseClerk(),
// Helper for debugging
debug() {
console.log('Auth Config:', {
NODE_ENV: process.env.NODE_ENV,
CLERK_ENABLED: process.env.NEXT_PUBLIC_CLERK_ENABLED,
VERCEL_ENV: process.env.VERCEL_ENV,
HAS_PUBLISHABLE_KEY: !!process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
USE_CLERK: this.useClerk,
})
},
}
Accessing Auth State
Kit provides different APIs for accessing authentication state depending on whether you are in a Server Component or a Client Component.
Server-Side
Calling
auth() directly from @clerk/nextjs/server will throw an error when Clerk is disabled (demo mode, test mode). The Clerk SDK initializes at import time, so even a static import { auth } from '@clerk/nextjs/server' at the top of a file will fail. Always use getServerAuth() instead — it uses dynamic imports and returns mock data when Clerk is not active.In Server Components, API routes, and Server Actions, use the
getServerAuth() helper. It dynamically imports Clerk's auth() function in production and returns mock data in test mode — preventing the Clerk SDK from being bundled when it is not needed:src/lib/auth/server-helpers.ts
export async function getServerAuth() {
// Test mode: Return mock auth data
if (!shouldUseClerk()) {
return {
userId: testUser.id,
sessionId: 'test-session-123',
sessionClaims: null,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
orgPermissions: null,
has: () => false,
debug: () => null,
}
}
// Production mode: Use real Clerk auth with dynamic import
try {
const { auth } = await import('@clerk/nextjs/server')
return await auth()
} catch (error) {
console.error('[Server Auth] Failed to load Clerk auth:', error)
throw new Error('Authentication unavailable')
}
}
There is also
getServerUser() for fetching the full user object (email, name, avatar) on the server.Usage:
typescript
import { getServerAuth, getServerUser } from '@/lib/auth/server-helpers'
// In a Server Component or API route:
const { userId } = await getServerAuth()
const user = await getServerUser()
Client-Side
In Client Components, use the conditional hooks from
@/lib/auth/use-conditional-auth. These wrappers call Clerk hooks in production and return mock data in test mode:typescript
import { useConditionalAuth, useConditionalUser } from '@/lib/auth/use-conditional-auth'
function MyComponent() {
const { userId, isSignedIn } = useConditionalAuth()
const { user } = useConditionalUser()
if (!isSignedIn) return <p>Please sign in</p>
return <p>Hello, {user?.firstName}</p>
}
Always use the conditional hooks (
useConditionalAuth, useConditionalUser) instead of importing directly from @clerk/nextjs. The conditional hooks ensure your components work in all environments — production, test, and demo mode.Clerk-to-Database Sync
Clerk manages user identity (email, password, social accounts), but your application also needs user data in the database for relationships, subscriptions, credits, and queries. Kit bridges this gap with webhook-based synchronization.
When Clerk fires a user event, the webhook handler at
/api/webhooks/clerk processes it:| Event | Action |
|---|---|
user.created | Creates a database record with Free Tier, initial credits (20), and a 30-day reset date |
user.updated | Updates email and name in the database |
user.deleted | Removes the user record from the database |
The webhook uses SVIX signature verification to ensure requests are genuinely from Clerk, and upsert operations for idempotency (safe to retry without creating duplicates).
For more details on setting up the webhook, see Setup & Providers. For the database user provider pattern, see User Management.
Key Files Reference
| File | Purpose |
|---|---|
apps/boilerplate/src/lib/auth/config.ts | Central environment detection (shouldUseClerk(), authConfig, testUser) |
apps/boilerplate/src/lib/auth/server-helpers.ts | Server-side auth helpers (getServerAuth(), getServerUser()) |
apps/boilerplate/src/lib/auth/use-conditional-auth.ts | Client-side conditional hooks (useConditionalAuth(), useConditionalUser()) |
apps/boilerplate/src/lib/auth/sync-user.ts | On-demand Clerk-to-database sync (syncUserFromClerk(), ensureUserExists()) |
apps/boilerplate/src/lib/auth/test-helpers.tsx | Mock auth components and hooks for testing |
apps/boilerplate/src/middleware.ts | Route protection with Clerk middleware |
apps/boilerplate/src/app/layout.tsx | Root layout with conditional ClerkProvider |
apps/boilerplate/src/app/(auth)/layout.tsx | Auth pages layout with error boundary |
apps/boilerplate/src/components/auth/auth-form.tsx | Universal sign-in/sign-up form (works in all environments) |
apps/boilerplate/src/app/api/webhooks/clerk/route.ts | Clerk webhook handler for database sync |
apps/boilerplate/src/providers/db-user-provider.tsx | React context providing database user ID to dashboard |