Authentication Overview

How Clerk authentication is integrated in nextsaas.ai Kit with environment-aware routing and webhook sync

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.

How Authentication Works

The authentication system connects four layers that work together to protect your application:
  1. Middleware intercepts every request and checks whether the route is public or protected. Protected routes require a valid Clerk session.
  2. ClerkProvider wraps your application and makes auth state available to all components — both server and client.
  3. Auth Hooks and Helpers let you access the current user in Server Components (getServerAuth()) and Client Components (useConditionalAuth()).
  4. 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 dynamic prop 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 ClerkProvider is 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:
RoutePurposeClerk Routing
/loginSign-in pagerouting="hash" renders <SignIn />
/registerSign-up pagerouting="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.

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.
ModeWhen ActiveBehavior
ProductionVERCEL_ENV=productionReal Clerk authentication. Cannot be overridden.
Test / CINODE_ENV=test or CLERK_ENABLED=falseMock auth with test user. Clerk SDK is not loaded.
DemoNEXT_PUBLIC_DEMO_MODE=trueDemo login form with tier switching. No real auth.
DevelopmentDefaultUses 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:
  1. Demo Mode (priority 0) — Always bypasses Clerk, even in production. This enables public demo deployments without real authentication.
  2. Production (priority 1) — Always uses Clerk. This is a safety measure — production can never accidentally run in test mode.
  3. Test Environment (priority 2) — Never uses Clerk. Detected via NEXT_PUBLIC_CLERK_ENABLED=false or NODE_ENV=test.
  4. Development (priority 3) — Uses Clerk only if NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is 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

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>
}

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:
EventAction
user.createdCreates a database record with Free Tier, initial credits (20), and a 30-day reset date
user.updatedUpdates email and name in the database
user.deletedRemoves 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

FilePurpose
apps/boilerplate/src/lib/auth/config.tsCentral environment detection (shouldUseClerk(), authConfig, testUser)
apps/boilerplate/src/lib/auth/server-helpers.tsServer-side auth helpers (getServerAuth(), getServerUser())
apps/boilerplate/src/lib/auth/use-conditional-auth.tsClient-side conditional hooks (useConditionalAuth(), useConditionalUser())
apps/boilerplate/src/lib/auth/sync-user.tsOn-demand Clerk-to-database sync (syncUserFromClerk(), ensureUserExists())
apps/boilerplate/src/lib/auth/test-helpers.tsxMock auth components and hooks for testing
apps/boilerplate/src/middleware.tsRoute protection with Clerk middleware
apps/boilerplate/src/app/layout.tsxRoot layout with conditional ClerkProvider
apps/boilerplate/src/app/(auth)/layout.tsxAuth pages layout with error boundary
apps/boilerplate/src/components/auth/auth-form.tsxUniversal sign-in/sign-up form (works in all environments)
apps/boilerplate/src/app/api/webhooks/clerk/route.tsClerk webhook handler for database sync
apps/boilerplate/src/providers/db-user-provider.tsxReact context providing database user ID to dashboard