Nurture TechnologiesNurture Tech
Integrations20 min read·July 17, 2026

Clerk Authentication Integration GuideSecure Auth for Modern SaaS Applications

ClerkReactNext.jsVueAngularNode.jsNestJSFastAPILaravelDjangoGoSpring BootASP.NET CoreRust

Building authentication from scratch is expensive and risky. Clerk gives SaaS products enterprise-grade auth, social logins, organizations, and RBAC in days. This guide covers the full integration across every major frontend and backend stack.

Every SaaS product needs authentication. The question is never whether to build it it is how fast you can implement it without creating security liabilities that cost you users, data, or both.

Building authentication from scratch is one of the most common and most expensive mistakes early-stage SaaS teams make. Password hashing, session management, OAuth flows, multi-factor authentication, team invitations, role-based access control each is a non-trivial engineering problem that takes weeks to implement correctly and months to harden against real-world attacks.

Clerk solves this. It is an authentication and user management platform designed specifically for modern SaaS applications. It handles everything from email and password login to organizations, team invitations, roles, and enterprise SSO, and integrates directly with every major frontend and backend stack.

Overview

Clerk provides a complete authentication layer as a managed service. Your application delegates all identity concerns to Clerk, which handles credential storage, session management, token issuance, social OAuth flows, MFA, and organization management. Your backend validates the short-lived JWTs Clerk issues on every protected request. Your frontend uses Clerk's SDK to render authentication UI and read the current session state.

  • Pre-built, customizable SignIn and SignUp components that match your brand in minutes
  • Social logins for Google, GitHub, Microsoft, Apple, and 20+ other providers with zero backend code
  • Organization and team management for multi-tenant SaaS applications
  • Role-based access control with custom roles and fine-grained permissions
  • Multi-factor authentication with TOTP apps, SMS, and backup codes
  • Short-lived JWTs for stateless backend validation across any language or framework
  • Clerk webhooks that fire on user creation, session start, role changes, and organization events

Business Benefits

Faster Development

A full-featured authentication system social logins, MFA, organizations, role management takes 4 to 8 weeks to build correctly from scratch. Clerk reduces that to 1 to 2 days. The engineering time saved goes directly toward product features that differentiate your business. For early-stage teams, shipping auth in days rather than months compresses the time to first paying customer significantly.

Enterprise-Grade Security Out of the Box

Clerk handles the security engineering that most product teams get wrong: brute-force protection, credential stuffing detection, secure password hashing, token rotation, and session revocation. These are not features you configure they are enforced by default. Clerk is SOC 2 Type 2 certified and GDPR compliant, which satisfies the initial security questionnaires from enterprise buyers without any additional work.

User Onboarding That Converts

Clerk's pre-built components handle the full onboarding flow: email verification, social login, progressive profile completion, and magic link sign-in. Each component is customizable to match your brand. The result is an onboarding experience that converts at the same rate as established consumer products, without your team spending weeks building and iterating on registration flows.

Multi-Tenant Organization Management

Clerk's Organizations feature gives every B2B SaaS product team-based access control out of the box. Each organization has its own member list, invitations, and role assignments. Members can belong to multiple organizations, switch between them in the UI, and carry different roles in each. This eliminates weeks of database schema design and UI work that would otherwise be required to support team accounts.

Reduced Authentication Risk

Authentication is one of the highest-risk engineering domains. A vulnerability in your auth layer exposes user data, enables account takeover, and can result in regulatory penalties. Delegating auth to Clerk means security incidents in the authentication layer are Clerk's responsibility to detect and respond to, not yours. Clerk's security team monitors for threats 24 hours a day, patches vulnerabilities within hours, and publishes their security practices publicly.

Compliance and Audit Readiness

Enterprise buyers require SOC 2 compliance, GDPR data processing agreements, and audit logs of authentication events. Clerk provides all three. Every sign-in, sign-out, role change, and failed attempt is logged in Clerk's audit log. GDPR deletion requests are handled by a single API call. When your first enterprise customer sends a security questionnaire, Clerk's compliance documentation answers the auth sections for you.

Architecture Overview

Clerk sits between your frontend and your backend as the identity layer. Your frontend uses Clerk's SDK to render authentication UI, read session state, and get the current user's JWT. Your backend uses Clerk's SDK or a JWT library to validate that token on every protected request. Your application data is linked to the Clerk user ID as a foreign key.

clerk-architecture.mmd
flowchart LR
  USR["User / Browser"] --> FE["Frontend\nReact / Next.js / Vue"]
  FE -- "SignIn / SignUp\nUI Components" --> CLERK["Clerk\nAuthentication Layer"]
  CLERK -- "Short-lived JWT\n+ Session Cookie" --> FE
  FE -- "API request\n+ Bearer JWT" --> BE["Backend API\nNode.js / Django / Go..."]
  BE -- "Verify JWT\nusing Clerk JWKS" --> CLERK
  CLERK -- "Public Key" --> BE
  BE -- "User ID as FK" --> DB["Database\nApplication Data"]
  CLERK -- "Webhooks\non user events" --> BE

The core architectural rule: Clerk handles identity, your backend handles business logic, your database handles application data. Your frontend never calls Clerk's secret key API it only reads the session state. Your backend validates the JWT on every protected request using Clerk's public JWKS endpoint.

Authentication Flow

  1. 1User visits your application and is redirected to Clerk's hosted SignIn page or sees Clerk's embedded component
  2. 2User authenticates via email/password, social provider, magic link, or passkey
  3. 3Clerk validates credentials, triggers MFA if enabled, and issues a short-lived JWT (default 60 seconds)
  4. 4Frontend SDK stores the session cookie and makes the JWT available via getToken()
  5. 5Frontend adds the JWT as a Bearer token in the Authorization header of every API request
  6. 6Backend middleware validates the JWT against Clerk's JWKS public endpoint on every request
  7. 7Business logic runs with the verified user ID never trust the user ID from the frontend directly
  8. 8Clerk webhooks fire for user creation, organization events, and role changes, triggering your automation

Frontend Integration

Clerk provides framework-specific SDKs for React, Next.js, and Vue, and a vanilla JavaScript SDK for every other framework. The core pattern is identical across all frameworks: wrap your application in a Clerk provider, use the sign-in and sign-up components or hooks, and read the current user's session state from Clerk's context.

React

$npm install @clerk/clerk-react
src/main.tsx
import { ClerkProvider, SignedIn, SignedOut, SignInButton, UserButton, useUser } from '@clerk/clerk-react'

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY

function App() {
  return (
    <ClerkProvider publishableKey={PUBLISHABLE_KEY}>
      <Header />
      <main>
        <SignedOut>
          <SignInButton mode="modal">
            <button className="btn-primary">Sign In</button>
          </SignInButton>
        </SignedOut>
        <SignedIn>
          <Dashboard />
        </SignedIn>
      </main>
    </ClerkProvider>
  )
}

function Header() {
  return (
    <header className="flex justify-between p-4">
      <span className="font-bold">Your App</span>
      <SignedIn>
        <UserButton afterSignOutUrl="/" />
      </SignedIn>
    </header>
  )
}

function Dashboard() {
  const { user } = useUser()
  return <h1>Welcome, {user?.firstName}</h1>
}

Next.js

$npm install @clerk/nextjs
middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)', '/api/webhooks(.*)'])

export default clerkMiddleware(async (auth, req) => {
  if (!isPublicRoute(req)) {
    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)(.*)'],
}
app/dashboard/page.tsx
import { auth, currentUser } from '@clerk/nextjs/server'

export default async function DashboardPage() {
  const { userId } = await auth()
  if (!userId) return null

  const user = await currentUser()

  return (
    <div>
      <h1>Welcome, {user?.firstName}</h1>
      <p>Your Clerk user ID: {userId}</p>
    </div>
  )
}

Vue.js, Nuxt

$npm install @clerk/vue
plugins/clerk.ts
// Nuxt plugin
import { clerkPlugin } from '@clerk/vue'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(clerkPlugin, {
    publishableKey: useRuntimeConfig().public.clerkPublishableKey,
  })
})
components/AppHeader.vue
<script setup lang="ts">
import { SignedIn, SignedOut, UserButton, SignInButton } from '@clerk/vue'
import { useUser } from '@clerk/vue'

const { user } = useUser()
</script>

<template>
  <header class="flex justify-between p-4">
    <span class="font-bold">Your App</span>
    <SignedIn>
      <p>Hello, {{ user?.firstName }}</p>
      <UserButton after-sign-out-url="/" />
    </SignedIn>
    <SignedOut>
      <SignInButton mode="modal">
        <button>Sign In</button>
      </SignInButton>
    </SignedOut>
  </header>
</template>

Angular

$npm install @clerk/clerk-js
src/app/services/clerk.service.ts
import { Injectable } from '@angular/core'
import Clerk from '@clerk/clerk-js'
import { BehaviorSubject } from 'rxjs'

@Injectable({ providedIn: 'root' })
export class ClerkService {
  private clerk!: Clerk
  readonly user$ = new BehaviorSubject<any>(null)
  readonly isLoaded$ = new BehaviorSubject(false)

  async initialize() {
    this.clerk = new Clerk(import.meta.env['NG_APP_CLERK_PUBLISHABLE_KEY'])
    await this.clerk.load()
    this.isLoaded$.next(true)
    this.user$.next(this.clerk.user)
    this.clerk.addListener(({ user }) => this.user$.next(user))
  }

  openSignIn() { this.clerk.openSignIn({}) }
  openSignUp() { this.clerk.openSignUp({}) }
  signOut() { return this.clerk.signOut() }

  async getToken(): Promise<string | null> {
    return this.clerk.session?.getToken() ?? null
  }
}
src/app/interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http'
import { inject } from '@angular/core'
import { from, switchMap } from 'rxjs'
import { ClerkService } from '../services/clerk.service'

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const clerk = inject(ClerkService)
  return from(clerk.getToken()).pipe(
    switchMap((token) => {
      if (!token) return next(req)
      return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }))
    })
  )
}

Svelte

$npm install @clerk/clerk-js
src/lib/ClerkProvider.svelte
<script lang="ts">
  import Clerk from '@clerk/clerk-js'
  import { setContext, onMount } from 'svelte'
  import { writable } from 'svelte/store'

  export let publishableKey: string

  const clerk = writable<Clerk | null>(null)
  const user = writable<any>(null)

  setContext('clerk', clerk)
  setContext('user', user)

  onMount(async () => {
    const instance = new Clerk(publishableKey)
    await instance.load()
    clerk.set(instance)
    user.set(instance.user)
    instance.addListener(({ user: u }) => user.set(u))
  })
</script>

<slot />
src/lib/SignInButton.svelte
<script lang="ts">
  import { getContext } from 'svelte'
  import type { Writable } from 'svelte/store'
  import type Clerk from '@clerk/clerk-js'

  const clerk = getContext<Writable<Clerk | null>>('clerk')

  function openSignIn() {
    $clerk?.openSignIn({})
  }
</script>

<button on:click={openSignIn}><slot>Sign In</slot></button>

Solid.js

src/contexts/clerk.tsx
import Clerk from '@clerk/clerk-js'
import { createContext, createSignal, onMount, useContext, JSX } from 'solid-js'

const ClerkContext = createContext<{ clerk: () => Clerk | null; user: () => any }>()

export function ClerkProvider(props: { publishableKey: string; children: JSX.Element }) {
  const [clerk, setClerk] = createSignal<Clerk | null>(null)
  const [user, setUser] = createSignal<any>(null)

  onMount(async () => {
    const instance = new Clerk(props.publishableKey)
    await instance.load()
    setClerk(instance)
    setUser(instance.user)
    instance.addListener(({ user: u }) => setUser(u))
  })

  return (
    <ClerkContext.Provider value={{ clerk, user }}>
      {props.children}
    </ClerkContext.Provider>
  )
}

export const useClerk = () => useContext(ClerkContext)!

Qwik

src/routes/layout.tsx
import { component$, useVisibleTask$, useStore } from '@builder.io/qwik'
import Clerk from '@clerk/clerk-js'

export const ClerkLayout = component$(() => {
  const state = useStore<{ user: any; loaded: boolean }>({ user: null, loaded: false })

  useVisibleTask$(async () => {
    const clerk = new Clerk(import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY as string)
    await clerk.load()
    state.user = clerk.user
    state.loaded = true
    clerk.addListener(({ user }) => { state.user = user })
  })

  return (
    <div>
      {state.loaded && (
        state.user
          ? <span>Hello, {state.user.firstName}</span>
          : <button onClick$={() => (window as any).Clerk?.openSignIn({})}>Sign In</button>
      )}
      <slot />
    </div>
  )
})

Astro

$npm install @clerk/astro
astro.config.mjs
import { defineConfig } from 'astro/config'
import clerk from '@clerk/astro'

export default defineConfig({
  integrations: [clerk()],
})
src/pages/dashboard.astro
---
import { auth } from '@clerk/astro/server'

const { userId } = auth()
if (!userId) return Astro.redirect('/sign-in')
---
<html>
  <body>
    <h1>Dashboard</h1>
    <p>User ID: {userId}</p>
  </body>
</html>

Backend Integration

Every protected backend endpoint must validate the Clerk JWT on every request. The JWT is short-lived (60 seconds by default) and signed with Clerk's private key. Your backend verifies the signature using Clerk's public JWKS endpoint. The validated token contains the user ID, organization ID, and any public metadata you have set on the user.

Node.js / Express

$npm install @clerk/backend express
src/middleware/clerk-auth.ts
import { createClerkClient } from '@clerk/backend'
import type { Request, Response, NextFunction } from 'express'

const clerkClient = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY! })

export async function requireAuth(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.replace('Bearer ', '')
  if (!token) return res.status(401).json({ error: 'No token provided' })

  try {
    const { sub: userId, org_id: orgId } = await clerkClient.verifyToken(token)
    ;(req as any).userId = userId
    ;(req as any).orgId = orgId
    next()
  } catch {
    res.status(401).json({ error: 'Invalid or expired token' })
  }
}

// Usage
import express from 'express'
const app = express()

app.get('/api/profile', requireAuth, async (req, res) => {
  const userId = (req as any).userId
  const user = await clerkClient.users.getUser(userId)
  res.json({ id: user.id, email: user.emailAddresses[0]?.emailAddress })
})

NestJS

$npm install @clerk/backend @nestjs/passport passport
src/auth/clerk.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'
import { createClerkClient } from '@clerk/backend'
import { ConfigService } from '@nestjs/config'

@Injectable()
export class ClerkAuthGuard implements CanActivate {
  private readonly clerk

  constructor(private readonly config: ConfigService) {
    this.clerk = createClerkClient({ secretKey: this.config.getOrThrow('CLERK_SECRET_KEY') })
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest()
    const token = req.headers.authorization?.replace('Bearer ', '')
    if (!token) throw new UnauthorizedException()

    try {
      const payload = await this.clerk.verifyToken(token)
      req.userId = payload.sub
      req.orgId = payload.org_id
      return true
    } catch {
      throw new UnauthorizedException('Invalid token')
    }
  }
}
src/users/users.controller.ts
import { Controller, Get, Req, UseGuards } from '@nestjs/common'
import { ClerkAuthGuard } from '../auth/clerk.guard'

@Controller('api')
@UseGuards(ClerkAuthGuard)
export class UsersController {
  @Get('profile')
  getProfile(@Req() req: any) {
    return { userId: req.userId, orgId: req.orgId }
  }
}

Python FastAPI

$pip install clerk-backend-api python-jose httpx
app/auth/clerk.py
import httpx
from jose import jwt, JWTError
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from functools import lru_cache
from app.core.config import settings

security = HTTPBearer()


@lru_cache(maxsize=1)
def get_clerk_jwks() -> dict:
    res = httpx.get(f"https://api.clerk.com/v1/jwks", headers={"Authorization": f"Bearer {settings.CLERK_SECRET_KEY}"})
    return res.json()


async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> dict:
    token = credentials.credentials
    try:
        jwks = get_clerk_jwks()
        payload = jwt.decode(token, jwks, algorithms=["RS256"], options={"verify_aud": False})
        user_id: str = payload.get("sub")
        if not user_id:
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
        return {"user_id": user_id, "org_id": payload.get("org_id")}
    except JWTError:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")


# Usage in a route
from fastapi import APIRouter
router = APIRouter()

@router.get("/api/profile")
async def profile(current_user: dict = Depends(get_current_user)):
    return current_user

Laravel

$composer require firebase/php-jwt
app/Http/Middleware/ClerkAuth.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class ClerkAuth
{
    public function handle(Request $request, Closure $next): mixed
    {
        $token = $request->bearerToken();
        if (!$token) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        try {
            $jwks = Cache::remember('clerk_jwks', 300, function () {
                return Http::withToken(config('services.clerk.secret_key'))
                    ->get('https://api.clerk.com/v1/jwks')
                    ->json();
            });

            $keys = JWK::parseKeySet($jwks);
            $payload = JWT::decode($token, $keys);

            $request->merge([
                'clerk_user_id' => $payload->sub,
                'clerk_org_id' => $payload->org_id ?? null,
            ]);

            return $next($request);
        } catch (\Exception $e) {
            return response()->json(['error' => 'Invalid token'], 401);
        }
    }
}

Django

$pip install PyJWT requests
auth/middleware.py
import jwt
import requests
from functools import lru_cache
from django.http import JsonResponse
from django.conf import settings
from jwt.algorithms import RSAAlgorithm


@lru_cache(maxsize=1)
def get_clerk_public_keys():
    res = requests.get(
        "https://api.clerk.com/v1/jwks",
        headers={"Authorization": f"Bearer {settings.CLERK_SECRET_KEY}"},
    )
    jwks = res.json()
    return {key["kid"]: RSAAlgorithm.from_jwk(key) for key in jwks["keys"]}


class ClerkAuthMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if not request.path.startswith("/api/"):
            return self.get_response(request)

        auth = request.headers.get("Authorization", "")
        if not auth.startswith("Bearer "):
            return JsonResponse({"error": "Unauthorized"}, status=401)

        token = auth[7:]
        try:
            header = jwt.get_unverified_header(token)
            keys = get_clerk_public_keys()
            public_key = keys[header["kid"]]
            payload = jwt.decode(token, public_key, algorithms=["RS256"], options={"verify_aud": False})
            request.clerk_user_id = payload["sub"]
            request.clerk_org_id = payload.get("org_id")
        except Exception:
            return JsonResponse({"error": "Invalid token"}, status=401)

        return self.get_response(request)

Go

$go get github.com/golang-jwt/jwt/v5 github.com/lestrrat-go/jwx/v2/jwk
middleware/clerk.go
package middleware

import (
    "context"
    "net/http"
    "strings"
    "time"

    "github.com/golang-jwt/jwt/v5"
    "github.com/lestrrat-go/jwx/v2/jwk"
)

type contextKey string

const UserIDKey contextKey = "clerkUserID"
const OrgIDKey contextKey = "clerkOrgID"

var jwksCache jwk.Set
var jwksExpiry time.Time

func getJWKS() (jwk.Set, error) {
    if jwksCache != nil && time.Now().Before(jwksExpiry) {
        return jwksCache, nil
    }
    set, err := jwk.Fetch(context.Background(), "https://api.clerk.com/v1/jwks")
    if err != nil {
        return nil, err
    }
    jwksCache = set
    jwksExpiry = time.Now().Add(5 * time.Minute)
    return set, nil
}

func ClerkAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        auth := r.Header.Get("Authorization")
        if !strings.HasPrefix(auth, "Bearer ") {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        tokenStr := auth[7:]
        jwks, err := getJWKS()
        if err != nil {
            http.Error(w, "Failed to fetch JWKS", http.StatusInternalServerError)
            return
        }
        token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
            kid, _ := t.Header["kid"].(string)
            key, found := jwks.LookupKeyID(kid)
            if !found {
                return nil, jwt.ErrTokenSignatureInvalid
            }
            var rawKey interface{}
            _ = key.Raw(&rawKey)
            return rawKey, nil
        })
        if err != nil || !token.Valid {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
        claims, _ := token.Claims.(jwt.MapClaims)
        ctx := context.WithValue(r.Context(), UserIDKey, claims["sub"])
        ctx = context.WithValue(ctx, OrgIDKey, claims["org_id"])
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Spring Boot

ClerkJwtFilter.java
@Component
public class ClerkJwtFilter extends OncePerRequestFilter {

    private final String clerkJwksUrl = "https://api.clerk.com/v1/jwks";
    private JwkProvider jwkProvider;

    @PostConstruct
    public void init() {
        this.jwkProvider = new JwkProviderBuilder(clerkJwksUrl)
            .cached(10, 60, TimeUnit.MINUTES)
            .build();
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {
        String auth = request.getHeader("Authorization");
        if (auth == null || !auth.startsWith("Bearer ")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        String token = auth.substring(7);
        try {
            DecodedJWT jwt = JWT.decode(token);
            Jwk jwk = jwkProvider.get(jwt.getKeyId());
            Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null);
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT verified = verifier.verify(token);

            request.setAttribute("clerkUserId", verified.getSubject());
            request.setAttribute("clerkOrgId", verified.getClaim("org_id").asString());
            chain.doFilter(request, response);
        } catch (Exception e) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        }
    }
}

ASP.NET Core

Program.cs
// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = $"https://clerk.{builder.Configuration["Clerk:Domain"]}";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = false,
            ValidateIssuer = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
        };
        options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
            $"https://api.clerk.com/v1/jwks",
            new JwksRetriever(),
            TimeSpan.FromMinutes(5),
            TimeSpan.FromMinutes(1)
        );
    });

builder.Services.AddAuthorization();

// Controller usage
[ApiController]
[Route("api")]
[Authorize]
public class ProfileController : ControllerBase
{
    [HttpGet("profile")]
    public IActionResult GetProfile()
    {
        var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        var orgId = User.FindFirst("org_id")?.Value;
        return Ok(new { userId, orgId });
    }
}

Rust

$# Cargo.toml: axum = "0.7", jsonwebtoken = "9", reqwest = { features = ["json"] }, serde_json = "1"
src/middleware/clerk.rs
use axum::{extract::Request, http::StatusCode, middleware::Next, response::Response};
use jsonwebtoken::{decode, decode_header, DecodingKey, Validation, Algorithm};
use reqwest::Client;
use serde_json::Value;
use std::sync::OnceLock;

static JWKS: OnceLock<Value> = OnceLock::new();

async fn fetch_jwks() -> Value {
    Client::new()
        .get("https://api.clerk.com/v1/jwks")
        .send().await.unwrap()
        .json().await.unwrap()
}

pub async fn clerk_auth(mut request: Request, next: Next) -> Result<Response, StatusCode> {
    let token = request
        .headers()
        .get("Authorization")
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.strip_prefix("Bearer "))
        .ok_or(StatusCode::UNAUTHORIZED)?;

    let header = decode_header(token).map_err(|_| StatusCode::UNAUTHORIZED)?;
    let kid = header.kid.ok_or(StatusCode::UNAUTHORIZED)?;

    let jwks = JWKS.get_or_init(|| futures::executor::block_on(fetch_jwks()));
    let key_data = jwks["keys"].as_array().unwrap().iter()
        .find(|k| k["kid"].as_str() == Some(&kid))
        .ok_or(StatusCode::UNAUTHORIZED)?;

    let decoding_key = DecodingKey::from_rsa_components(
        key_data["n"].as_str().unwrap(),
        key_data["e"].as_str().unwrap(),
    ).map_err(|_| StatusCode::UNAUTHORIZED)?;

    let mut validation = Validation::new(Algorithm::RS256);
    validation.validate_aud = false;

    let token_data = decode::<serde_json::Map<String, Value>>(token, &decoding_key, &validation)
        .map_err(|_| StatusCode::UNAUTHORIZED)?;

    request.extensions_mut().insert(token_data.claims["sub"].as_str().unwrap_or("").to_string());
    Ok(next.run(request).await)
}

Authentication

Clerk authentication involves a publishable key for the frontend, a secret key for backend API calls, and webhook signing secrets for validating Clerk event notifications. The publishable key is safe to expose in client-side code. The secret key and webhook signing secret must never appear in client-side code, version control, or logs.

Clerk App Setup

  1. 1Sign in to clerk.com and create a new application give it your product name
  2. 2Select the sign-in methods to enable: email, password, Google, GitHub, etc.
  3. 3Copy the Publishable Key (safe for frontend) from the API Keys panel
  4. 4Copy the Secret Key (backend only) from the API Keys panel
  5. 5In Webhooks, create a new endpoint pointing to your backend URL for Clerk events
  6. 6Copy the Webhook Signing Secret from the webhook configuration
  7. 7Enable Organizations in the Organizations settings if your app is multi-tenant
  8. 8Configure custom roles in the Roles section under Organizations
  9. 9Set up Email Templates in the Emails section to match your brand

Environment Variables

.env
# Frontend   safe to expose, prefixed for Next.js
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxxxx
# Or for Vite / React
VITE_CLERK_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxxxx

# Backend only   never put in client-side code or commit to version control
CLERK_SECRET_KEY=sk_live_xxxxxxxxxxxxx

# Webhook signing secret   used to validate Clerk event webhooks
CLERK_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx

# Redirect URLs after sign-in / sign-out
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard

Webhook Signature Validation

app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix'
import { headers } from 'next/headers'
import { WebhookEvent } from '@clerk/nextjs/server'

export async function POST(req: Request) {
  const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET
  if (!WEBHOOK_SECRET) throw new Error('Missing CLERK_WEBHOOK_SECRET')

  const headerPayload = await headers()
  const svix_id = headerPayload.get('svix-id')
  const svix_timestamp = headerPayload.get('svix-timestamp')
  const svix_signature = headerPayload.get('svix-signature')

  if (!svix_id || !svix_timestamp || !svix_signature) {
    return new Response('Missing svix headers', { status: 400 })
  }

  const payload = await req.json()
  const body = JSON.stringify(payload)

  const wh = new Webhook(WEBHOOK_SECRET)
  let event: WebhookEvent

  try {
    event = wh.verify(body, { 'svix-id': svix_id, 'svix-timestamp': svix_timestamp, 'svix-signature': svix_signature }) as WebhookEvent
  } catch {
    return new Response('Invalid webhook signature', { status: 401 })
  }

  // Process validated event
  switch (event.type) {
    case 'user.created':
      await handleUserCreated(event.data)
      break
    case 'organization.created':
      await handleOrgCreated(event.data)
      break
    case 'organizationMembership.created':
      await handleMemberAdded(event.data)
      break
  }

  return new Response('OK', { status: 200 })
}

async function handleUserCreated(data: any) {
  const email = data.email_addresses[0]?.email_address
  await db.users.create({ data: { clerkId: data.id, email } })
}

async function handleOrgCreated(data: any) {
  await db.organizations.create({ data: { clerkId: data.id, name: data.name } })
}

async function handleMemberAdded(data: any) {
  console.log('Member added to org:', data.organization.id)
}

JWT Validation Best Practices

  • Cache the Clerk JWKS response with a TTL of 5 minutes Clerk rotates keys periodically and fetching on every request adds latency
  • Always verify the JWT signature never decode and trust without verification
  • Use the sub claim as the user identifier in your database, not the email address
  • Set the JWT token lifetime to 60 seconds in Clerk dashboard for maximum security
  • On the backend, validate the token on every request never cache a validated user session in server memory
  • The secret key (sk_live_) must only ever be used server-side never expose it to the browser

Automation Examples

User Registered Provision and Notify

lib/webhooks/user-created.ts
import { WebClient as Slack } from '@slack/web-api'
import { Client as HubSpot } from '@hubspot/api-client'

const slack = new Slack(process.env.SLACK_BOT_TOKEN)
const hubspot = new HubSpot({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN })

export async function handleUserCreated(data: {
  id: string
  email_addresses: Array<{ email_address: string }>
  first_name: string | null
  last_name: string | null
}) {
  const email = data.email_addresses[0]?.email_address
  const name = [data.first_name, data.last_name].filter(Boolean).join(' ')

  // 1. Create user record in your database
  await db.users.create({ data: { clerkId: data.id, email, name } })

  // 2. Create contact in HubSpot
  await hubspot.crm.contacts.basicApi.create({
    properties: { email, firstname: data.first_name ?? '', lastname: data.last_name ?? '' },
  })

  // 3. Notify the growth team in Slack
  await slack.chat.postMessage({
    channel: process.env.SLACK_GROWTH_CHANNEL!,
    blocks: [
      {
        type: 'section',
        text: { type: 'mrkdwn', text: `:wave: *New user registered*\n*Name:* ${name || 'Unknown'}\n*Email:* ${email}` },
      },
    ],
  })
}

Organization Created Provision Resources

lib/webhooks/org-created.ts
export async function handleOrganizationCreated(data: {
  id: string
  name: string
  slug: string
  created_by: string
}) {
  // 1. Create org record in your database
  const org = await db.organizations.create({
    data: { clerkId: data.id, name: data.name, slug: data.slug },
  })

  // 2. Provision default resources for the new organization
  await db.workspaces.create({ data: { orgId: org.id, name: `${data.name}'s Workspace` } })

  // 3. Assign the creator the Owner role in your database
  const creator = await db.users.findUnique({ where: { clerkId: data.created_by } })
  if (creator) {
    await db.orgMemberships.create({
      data: { userId: creator.id, orgId: org.id, role: 'owner' },
    })
  }

  // 4. Send a welcome email via your email provider
  await sendEmail({
    to: creator?.email ?? '',
    template: 'org-welcome',
    data: { orgName: data.name },
  })
}

Role Changed Sync Permissions

lib/webhooks/role-changed.ts
export async function handleMembershipUpdated(data: {
  id: string
  role: string
  public_user_data: { user_id: string }
  organization: { id: string; name: string }
}) {
  const clerkUserId = data.public_user_data.user_id
  const clerkOrgId = data.organization.id

  // Sync the new role to your database
  const user = await db.users.findUnique({ where: { clerkId: clerkUserId } })
  const org = await db.organizations.findUnique({ where: { clerkId: clerkOrgId } })

  if (user && org) {
    await db.orgMemberships.upsert({
      where: { userId_orgId: { userId: user.id, orgId: org.id } },
      update: { role: data.role },
      create: { userId: user.id, orgId: org.id, role: data.role },
    })

    // If the user was granted admin, send them an email
    if (data.role === 'org:admin') {
      await sendEmail({
        to: user.email,
        template: 'role-upgraded',
        data: { orgName: data.organization.name, role: 'Admin' },
      })
    }
  }
}

CRM Integrations

HubSpot

integrations/clerk-hubspot.ts
import { Client } from '@hubspot/api-client'

const hubspot = new Client({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN })

export async function syncClerkUserToHubSpot(clerkUser: {
  id: string
  email: string
  firstName: string | null
  lastName: string | null
}) {
  // Check if contact already exists
  const existing = await hubspot.crm.contacts.searchApi.doSearch({
    filterGroups: [{ filters: [{ propertyName: 'email', operator: 'EQ', value: clerkUser.email }] }],
    properties: ['email', 'hs_object_id'],
    limit: 1,
    after: 0,
    sorts: [],
  })

  const properties = {
    email: clerkUser.email,
    firstname: clerkUser.firstName ?? '',
    lastname: clerkUser.lastName ?? '',
    clerk_user_id: clerkUser.id,
  }

  if (existing.total > 0) {
    const id = existing.results[0].id
    await hubspot.crm.contacts.basicApi.update(id, { properties })
  } else {
    await hubspot.crm.contacts.basicApi.create({ properties })
  }
}

Salesforce

integrations/clerk-salesforce.ts
import jsforce from 'jsforce'

const conn = new jsforce.Connection({ loginUrl: process.env.SF_LOGIN_URL })

export async function syncClerkUserToSalesforce(user: {
  id: string
  email: string
  firstName: string | null
  lastName: string | null
}) {
  await conn.login(process.env.SF_USERNAME!, process.env.SF_PASSWORD! + process.env.SF_SECURITY_TOKEN!)

  // Upsert on Clerk__User_ID__c custom field
  await (conn.sobject('Lead') as any).upsert(
    {
      Email: user.email,
      FirstName: user.firstName ?? '',
      LastName: user.lastName ?? 'Unknown',
      Clerk__User_ID__c: user.id,
      LeadSource: 'Web Signup',
    },
    'Clerk__User_ID__c'
  )
}

Zoho CRM

Zoho CRM does not have a push-based webhook for new leads, but you can create Zoho contacts directly from your Clerk webhook handler. When Clerk fires a user.created event, your backend calls the Zoho CRM v2 API at https://www.zohoapis.com/crm/v2/Contacts with an OAuth access token. Obtain the OAuth token using Zoho's server-to-server self-client flow and cache it with the expiry timestamp. Map Clerk's user ID to a custom field in Zoho so you can update the contact on subsequent Clerk events.

Custom CRM via Webhook

lib/webhooks/crm-sync.ts
interface CRMContact {
  externalId: string
  email: string
  name: string
  source: string
  metadata: Record<string, string>
}

export async function upsertCRMContact(user: {
  id: string
  email: string
  name: string
  orgId?: string
}) {
  const contact: CRMContact = {
    externalId: user.id,
    email: user.email,
    name: user.name,
    source: 'clerk_registration',
    metadata: {
      clerkUserId: user.id,
      ...(user.orgId && { clerkOrgId: user.orgId }),
    },
  }

  const res = await fetch(`${process.env.CRM_BASE_URL}/contacts/upsert`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': process.env.CRM_API_KEY!,
    },
    body: JSON.stringify(contact),
  })

  if (!res.ok) {
    throw new Error(`CRM upsert failed: ${res.status} ${await res.text()}`)
  }

  return res.json()
}

AI Integrations

OpenAI

app/api/ai/chat/route.ts
import { auth } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'
import OpenAI from 'openai'

const openai = new OpenAI()

export async function POST(req: NextRequest) {
  const { userId, orgId, sessionClaims } = await auth()
  if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const { message } = await req.json()
  const role = (sessionClaims?.org_role as string) ?? 'member'

  // Tailor the system prompt based on the user's role and organization
  const systemPrompt = orgId
    ? `You are an AI assistant for organization ${orgId}. The user's role is ${role}. ${role === 'org:admin' ? 'They have full access to all features.' : 'They have standard member access.'}`
    : 'You are a helpful AI assistant.'

  const completion = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: message },
    ],
  })

  return NextResponse.json({ reply: completion.choices[0].message.content })
}

Anthropic Claude

app/api/ai/assistant/route.ts
import { auth } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'
import Anthropic from '@anthropic-ai/sdk'

const claude = new Anthropic()

export async function POST(req: NextRequest) {
  const { userId, orgId, sessionClaims } = await auth()
  if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const { message } = await req.json()

  // Fetch user-specific context from your database
  const userContext = await db.users.findUnique({
    where: { clerkId: userId },
    select: { name: true, plan: true, preferences: true },
  })

  const response = await claude.messages.create({
    model: 'claude-sonnet-4-6',
    max_tokens: 2048,
    system: `You are a personalized AI assistant. The user is ${userContext?.name}. Plan: ${userContext?.plan}. Org: ${orgId ?? 'personal'}.`,
    messages: [{ role: 'user', content: message }],
  })

  const text = response.content.filter((b) => b.type === 'text').map((b) => b.text).join('')
  return NextResponse.json({ reply: text })
}
app/api/ai/search/route.ts
import { auth } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'
import { searchKnowledgeBase } from '@/lib/kb'
import OpenAI from 'openai'

const openai = new OpenAI()

export async function POST(req: NextRequest) {
  const { userId, orgId, sessionClaims } = await auth()
  if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const { query } = await req.json()
  const role = (sessionClaims?.org_role as string) ?? 'member'

  // Search only docs the user's role has access to
  const accessLevel = role === 'org:admin' ? 'all' : 'member'
  const docs = await searchKnowledgeBase(query, { orgId, accessLevel, topK: 3 })

  if (!docs.length) {
    return NextResponse.json({ answer: 'No relevant documentation found for your access level.' })
  }

  const context = docs.map((d) => `## ${d.title}\n${d.content}`).join('\n\n')

  const { choices } = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      { role: 'system', content: `Answer from this documentation only:\n${context}` },
      { role: 'user', content: query },
    ],
  })

  return NextResponse.json({
    answer: choices[0].message.content,
    sources: docs.map((d) => ({ title: d.title, url: d.url })),
  })
}

Security Considerations

Never Trust the Frontend Session Alone

The frontend session is controlled by the browser and can be tampered with. Every protected backend endpoint must validate the JWT independently on every request. The fact that a user is signed in on the frontend does not mean their request is authorized on the backend. Apply JWT validation middleware to every route that touches application data, never skip it for convenience during development.

Access Control

Role-based access control must be enforced on the backend. Frontend permission checks are for UX they hide UI elements from unauthorized users, which improves usability. They do not prevent a determined attacker from calling your API directly. Backend permission checks are the security boundary. Every mutation endpoint must verify the caller's role from the validated JWT claims before executing.

Token Rotation and Secrets Management

Clerk's secret key grants full access to your user database including read, write, and deletion. Store it in a secrets management service, not a .env file on the server. Rotate it if you suspect exposure. The publishable key is designed to be public expose it freely. The signing key for webhooks must be validated on every inbound event from Clerk to prevent arbitrary payloads from triggering your automation.

Data Protection

Clerk stores user credentials and session data. Your database stores application data linked to the Clerk user ID. Keep sensitive application data health information, financial records, private messages in your database, not in Clerk's user metadata. Clerk's public metadata is readable by the client, and private metadata is accessible to anyone with your secret key.

Audit Logging

Log every authentication event your backend processes from Clerk webhooks: user created, user signed in, role changed, organization created, member added. Include the Clerk user ID, organization ID, event type, and timestamp. For compliance-sensitive applications, ship these logs to an immutable log store (CloudWatch Logs with retention policy, Datadog Archive) so the audit trail cannot be modified after the fact.

Monitoring and Observability

Sentry

Attach the Clerk user ID to every Sentry event using Sentry's user context. In Next.js, read the userId from auth() in your global error boundary and set it on the Sentry scope. This links every error report to the specific user who triggered it, which dramatically reduces debugging time for user-reported issues. Tag events with the organization ID for multi-tenant applications so you can filter errors by org.

Datadog and Grafana

Track authentication metrics: sign-in success rate, sign-in failure rate by error type, new user registrations per day, organization creation rate, and JWT validation failure rate on your backend. Dashboard these alongside your application's conversion funnel metrics. A spike in sign-in failures often indicates a UX problem in the auth flow, a bug in your middleware, or an active credential stuffing attack.

Structured Logging

lib/auth-logger.ts
export function logAuthEvent(event: {
  type: 'jwt_validated' | 'jwt_rejected' | 'webhook_received' | 'webhook_rejected' | 'user_synced'
  userId?: string
  orgId?: string
  reason?: string
  durationMs?: number
}) {
  console.log(JSON.stringify({
    service: 'clerk-auth',
    ...event,
    timestamp: new Date().toISOString(),
  }))
}

// In JWT middleware:
logAuthEvent({ type: 'jwt_validated', userId: payload.sub, orgId: payload.org_id, durationMs: 12 })
// On failure:
logAuthEvent({ type: 'jwt_rejected', reason: 'token_expired', durationMs: 3 })

Webhook Health Monitoring

Clerk retries webhook delivery on failures and marks your endpoint as degraded if it consistently returns non-2xx responses. Monitor your webhook endpoint's success rate in Clerk's dashboard under Webhooks. Set up an alert if the failure rate exceeds 5% over a 10-minute window. A failing webhook endpoint means your user provisioning, CRM sync, and automation pipelines are silently falling behind.

Common Mistakes

Exposing the Clerk Secret Key on the Frontend

The Clerk secret key (sk_live_) gives read and write access to every user in your workspace. Prefixing it with NEXT_PUBLIC_ or VITE_ exposes it in your JavaScript bundle where any visitor can extract it from DevTools. Only the publishable key (pk_live_) belongs in client-side code. The secret key belongs in server-side environment variables and is used only for backend API calls and webhook validation.

Skipping Backend JWT Validation

A common mistake during development is trusting the user ID sent directly from the frontend in the request body rather than validating the JWT. This allows any client to impersonate any user by simply sending a different user ID. The validated sub claim from the JWT is the only trustworthy source of user identity on the backend. Apply JWT middleware to every protected route without exception.

Not Handling Webhook Events Idempotently

Clerk retries webhook delivery when your endpoint returns non-2xx responses. Without idempotency checks, a transient database error causes the same user.created event to be processed twice creating duplicate user records, duplicate CRM contacts, and duplicate Slack notifications. Use the Clerk event ID as an idempotency key: store it in your database and skip processing if it already exists.

Not Handling Organization Context in Multi-Tenant Queries

In multi-tenant applications, every database query that returns organization-scoped data must filter by organization ID from the JWT claims, not from the request body. A user who belongs to two organizations must not be able to read the second organization's data by sending the wrong org ID in the request. Extract org_id from the validated JWT claims and apply it as a WHERE clause on every scoped query.

Caching JWT Validation Results

Clerk JWTs are short-lived 60 seconds by default. Caching a validated JWT payload in server memory and reusing it across requests defeats the purpose of short-lived tokens. If a user's session is revoked or their role changes, cached validation results will serve stale permissions for up to the cache TTL. Validate the JWT on every request. The JWKS response should be cached, not the decoded token.

Production Deployment Checklist

  1. 1CLERK_SECRET_KEY and CLERK_WEBHOOK_SECRET stored in a secrets manager, not in .env files on the server
  2. 2NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY (or equivalent) is the only Clerk value exposed to the frontend
  3. 3JWT validation middleware applied to every protected backend route without exception
  4. 4Webhook handler validates the svix signature before processing any event data
  5. 5Webhook events processed idempotently using the Clerk event ID as a deduplication key
  6. 6Clerk JWKS response cached with a 5-minute TTL not the decoded JWT payload
  7. 7Organization ID extracted from the JWT claims on every multi-tenant query
  8. 8Role and permission checks enforced on the backend, not just in the frontend UI
  9. 9Clerk dashboard email addresses verified test with real providers, not only test accounts
  10. 10Social provider OAuth credentials (Google, GitHub) using production app credentials, not dev credentials
  11. 11Clerk webhook endpoint returning 200 within 5 seconds verify in Clerk's webhook delivery logs
  12. 12User deletion flow tested end-to-end: Clerk user deleted, application data cleaned up, CRM updated
Nurture Technologies

NEED HELP WITH YOUR STACK?

Nurture Technologies builds and maintains production-quality software for startups and businesses. If engineering problems are slowing you down, our team can help.

Talk to our team →
FAQ

FREQUENTLY ASKED QUESTIONS

What is Clerk and why use it instead of building authentication?+

Clerk is a managed authentication and user management platform designed for SaaS applications. Building authentication from scratch requires implementing password hashing, session management, OAuth flows for social providers, MFA, token rotation, brute-force protection, and compliance features typically 4 to 8 weeks of engineering time. Clerk provides all of this as a managed service with pre-built UI components, and integrates with every major frontend and backend stack. The business case is straightforward: the weeks saved on auth ship as product features instead.

Is Clerk safe for production SaaS applications?+

Yes. Clerk is SOC 2 Type 2 certified, GDPR compliant, and used in production by thousands of SaaS applications. Clerk handles credential storage, session management, brute-force protection, and security monitoring. Their security team responds to vulnerabilities within hours. The risk profile of using a specialized managed auth service is significantly lower than building and maintaining your own authentication, where a single misconfiguration can expose user credentials.

How do I validate Clerk JWTs on my backend?+

Fetch Clerk's public JWKS from https://api.clerk.com/v1/jwks and cache the response for 5 minutes. On every incoming request, extract the Bearer token from the Authorization header, decode the token header to get the kid (key ID), find the matching public key in the JWKS, and verify the JWT signature using RS256. The validated sub claim contains the Clerk user ID. Use the @clerk/backend SDK in Node.js for a one-line implementation, or use any JWT library in other languages.

How do I add Clerk to a Next.js application?+

Install @clerk/nextjs and add ClerkProvider to your root layout. Create a middleware.ts file at the root of your project using clerkMiddleware from @clerk/nextjs/server this protects all routes except the ones you explicitly mark as public. Use auth() in server components and server actions to get the current user's ID. Use useUser() and useAuth() in client components. Set NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY in your environment variables.

How does Clerk handle multi-tenant organization management?+

Clerk's Organizations feature provides built-in multi-tenant support. Each organization has members, roles, and invitations managed by Clerk. Members can belong to multiple organizations and switch between them in the UI. The active organization ID and the user's role within it are included in the JWT as org_id and org_role claims. Your backend extracts these from the validated JWT and uses them to scope database queries. You do not need to build your own team management UI or invitation flow.

How do I implement role-based access control with Clerk?+

Define custom roles in the Clerk dashboard under Organizations. Assign roles to users when they are added to an organization. The user's role appears in the JWT as the org_role claim. In Next.js, use auth().then(({sessionClaims}) => sessionClaims?.org_role) on the server to get the role. Protect server actions and API routes by checking the role from the JWT before executing sensitive operations. Use has({role: 'org:admin'}) from Clerk's auth() helper for convenient role checking.

How do I handle Clerk webhooks?+

Enable Webhooks in your Clerk dashboard and add your backend endpoint URL. Subscribe to the event types you need: user.created, organization.created, organizationMembership.created, and others. Install the svix package (Clerk uses Svix for webhook delivery) and use it to validate the webhook signature using your CLERK_WEBHOOK_SECRET before processing any payload. Respond with HTTP 200 immediately. Process webhook events idempotently using the event ID as a deduplication key to handle Clerk's retry behavior.

Does Clerk work with non-Next.js backends like Django, Go, or Laravel?+

Yes. Any backend that can verify a JWT using RS256 works with Clerk. Fetch Clerk's public JWKS from the api.clerk.com endpoint and use a JWT library in your language to verify tokens: PyJWT in Python, golang-jwt in Go, firebase/php-jwt in Laravel, jsonwebtoken in Node.js, System.IdentityModel.Tokens.Jwt in .NET. The validated token contains the user ID, organization ID, and role. Clerk's official backend SDKs are available for Node.js, Python, Ruby, and Go.

How do I sync Clerk users to my database?+

Use Clerk webhooks. Subscribe to user.created and user.updated events. When user.created fires, create a corresponding record in your database with the Clerk user ID as the primary key or as an indexed foreign key. Store only application-specific data in your database Clerk handles the authentication data. Use the Clerk user ID in your database queries to look up the application user from a validated JWT's sub claim. Never store passwords or session tokens in your database.

What social login providers does Clerk support?+

Clerk supports Google, GitHub, Microsoft, Apple, Facebook, LinkedIn, Discord, Spotify, Twitter/X, Twitch, TikTok, Notion, Coinbase, Hugging Face, GitLab, and others. Enable social providers in the Clerk dashboard under Configure → Social Connections. Enter the client ID and client secret from each provider's developer console. Clerk renders the sign-in buttons automatically no custom OAuth code required. Apple Sign In is required for iOS apps distributed through the App Store.

Does Clerk support multi-factor authentication?+

Yes. Clerk supports TOTP authenticator apps (Google Authenticator, Authy, 1Password), SMS codes, and backup codes. Enable MFA in the Clerk dashboard. Users can enroll in MFA through the Clerk UserProfile component with no additional code in your application. You can require MFA for specific roles or make it optional. Clerk's pre-built MFA enrollment and challenge UI handles the full flow, including backup code display and regeneration.

Can I use Clerk with a custom domain?+

Yes. Clerk supports custom domains on paid plans. You configure a subdomain on your own domain (for example, auth.yourdomain.com) as your Clerk frontend API domain. This removes Clerk-branded URLs from your authentication flow entirely. Users see your domain throughout the sign-in and sign-up flow. Custom domains are configured in the Clerk dashboard under Domains and require a CNAME record pointing to Clerk's infrastructure.

How do I migrate existing users from another auth system to Clerk?+

Clerk provides a user import API that accepts email addresses and hashed passwords in supported formats: bcrypt, scrypt, PBKDF2, md5, sha1, sha256, and argon2. Imported users can sign in immediately without resetting their password. For unsupported hash algorithms, import users without a password and trigger a password reset email as part of the migration. Test the import with a subset of users before running the full migration. Preserve your existing user IDs in Clerk's externalId field to maintain foreign key integrity in your database.

Is Clerk GDPR and HIPAA compliant?+

Clerk is SOC 2 Type 2 certified and GDPR compliant. They offer a Data Processing Agreement for GDPR compliance. HIPAA compliance for health applications requires a Business Associate Agreement, which Clerk offers on the Enterprise plan. For GDPR user deletion (right to erasure), call Clerk's delete user API and clean up application data in your database that references the Clerk user ID. Clerk also supports data residency options for organizations with specific regional data requirements.

What is the pricing model for Clerk?+

Clerk's pricing is based on monthly active users (MAU). The free plan includes up to 10,000 MAU with all core features including social logins, MFA, and organizations. Paid plans start at a per-MAU rate above the free threshold and add features like custom domains, advanced analytics, SAML SSO, and SLAs. For most early-stage SaaS applications, Clerk is free until you reach 10,000 active users per month at which point the revenue from those users typically far exceeds the Clerk subscription cost.