Skip to main content
Technology & EngineeringBaas342 lines

Clerk Auth

Clerk authentication service with pre-built UI components, session management, and multi-framework support

Quick Summary30 lines
You are an expert in Clerk for authentication, including its pre-built UI components, session management, webhook integrations, and multi-framework support (Next.js, React, Remix, Astro, Express).

## Key Points

- Use Clerk's pre-built `<SignIn />`, `<SignUp />`, and `<UserButton />` components to save development time; customize with the `appearance` prop or Clerk themes.
- Set up webhooks (`user.created`, `user.updated`, `user.deleted`) to keep your database synchronized with Clerk's user records.
- Use JWT templates to issue tokens compatible with your backend (Supabase, Convex, Hasura) rather than passing Clerk tokens directly.
- Protect routes at the middleware level using `clerkMiddleware` rather than checking auth in every page/route handler.
- Use organizations for multi-tenant applications — Clerk handles invitations, roles, and membership out of the box.
- **Not syncing users to your database**: Clerk manages user data externally; if your app needs to store user-related data, set up webhooks to create local user records on `user.created`.
- **Exposing the secret key**: `CLERK_SECRET_KEY` must never be in client-side code; use `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` for the browser.
- **Middleware matcher misconfiguration**: If the middleware matcher is too broad, static assets and Next.js internals get intercepted; use the recommended matcher pattern.
- **Webhook verification skipped**: Always verify webhook signatures using the `svix` library; unverified webhooks can be spoofed.
- **Token expiry in long-running operations**: Clerk JWTs have a short lifespan (60 seconds by default); always call `getToken()` immediately before each API request rather than caching the token.

## Quick Example

```bash
npm install @clerk/nextjs
```

```typescript
// .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
```
skilldb get baas-skills/Clerk AuthFull skill: 342 lines
Paste into your CLAUDE.md or agent config

Clerk — Authentication as a Service

You are an expert in Clerk for authentication, including its pre-built UI components, session management, webhook integrations, and multi-framework support (Next.js, React, Remix, Astro, Express).

Core Philosophy

Overview

Clerk is a complete user management and authentication platform. It provides drop-in UI components for sign-in, sign-up, and user profiles; supports email/password, magic links, SMS, OAuth, SAML SSO, and passkeys; handles session management with JWTs; offers organization and multi-tenant support; and integrates via SDKs for React, Next.js, Remix, Astro, Express, and more. Clerk is a managed service — there is no self-hosted option.

Setup & Configuration

Next.js Setup

npm install @clerk/nextjs
// .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

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

export default clerkMiddleware(async (auth, request) => {
  if (!isPublicRoute(request)) {
    await auth.protect();
  }
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

React (Vite) Setup

npm install @clerk/clerk-react
import { ClerkProvider } from '@clerk/clerk-react';

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;

function App() {
  return (
    <ClerkProvider publishableKey={PUBLISHABLE_KEY}>
      <MainApp />
    </ClerkProvider>
  );
}

Core Patterns

Pre-built UI Components

import { SignIn, SignUp, UserButton, UserProfile } from '@clerk/nextjs';

// Sign-in page
// app/sign-in/[[...sign-in]]/page.tsx
export default function SignInPage() {
  return (
    <div className="flex justify-center py-24">
      <SignIn />
    </div>
  );
}

// Sign-up page
// app/sign-up/[[...sign-up]]/page.tsx
export default function SignUpPage() {
  return (
    <div className="flex justify-center py-24">
      <SignUp />
    </div>
  );
}

// User button (avatar with dropdown menu)
function Navbar() {
  return (
    <nav>
      <UserButton afterSignOutUrl="/" />
    </nav>
  );
}

// Full user profile management
function SettingsPage() {
  return <UserProfile />;
}

Server-side Auth (Next.js App Router)

// In Server Components
import { currentUser, auth } from '@clerk/nextjs/server';

// Get the full user object
async function ProfilePage() {
  const user = await currentUser();
  if (!user) return <div>Not signed in</div>;
  return <div>Hello {user.firstName}</div>;
}

// Get just the auth state (lighter)
async function DashboardPage() {
  const { userId, orgId } = await auth();
  if (!userId) redirect('/sign-in');
  // userId is guaranteed to be set here
}
// In API routes
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export async function GET() {
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Fetch user-specific data
  return NextResponse.json({ userId });
}

Client-side Auth Hooks

import { useUser, useAuth, useSignIn, useClerk } from '@clerk/nextjs';

function Dashboard() {
  const { isLoaded, isSignedIn, user } = useUser();
  const { getToken, signOut } = useAuth();

  if (!isLoaded) return <div>Loading...</div>;
  if (!isSignedIn) return <div>Please sign in</div>;

  const handleApiCall = async () => {
    // Get a JWT for your backend
    const token = await getToken({ template: 'supabase' }); // or custom template
    await fetch('/api/data', {
      headers: { Authorization: `Bearer ${token}` },
    });
  };

  return (
    <div>
      <p>Welcome {user.firstName}</p>
      <p>Email: {user.primaryEmailAddress?.emailAddress}</p>
      <button onClick={handleApiCall}>Fetch Data</button>
      <button onClick={() => signOut()}>Sign Out</button>
    </div>
  );
}

Organizations (Multi-tenancy)

import {
  OrganizationSwitcher,
  CreateOrganization,
  OrganizationProfile,
} from '@clerk/nextjs';
import { auth } from '@clerk/nextjs/server';

// Organization switcher component
function Header() {
  return <OrganizationSwitcher />;
}

// Server-side: get active organization
async function OrgPage() {
  const { orgId, orgRole, orgSlug } = await auth();
  if (!orgId) return <div>Select an organization</div>;
  return <div>Current org: {orgSlug} (role: {orgRole})</div>;
}

Webhooks

// 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!;
  const headerPayload = await headers();
  const svixId = headerPayload.get('svix-id');
  const svixTimestamp = headerPayload.get('svix-timestamp');
  const svixSignature = headerPayload.get('svix-signature');

  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': svixId!,
      'svix-timestamp': svixTimestamp!,
      'svix-signature': svixSignature!,
    }) as WebhookEvent;
  } catch {
    return new Response('Verification failed', { status: 400 });
  }

  switch (event.type) {
    case 'user.created':
      // Sync user to your database
      await db.users.create({
        clerkId: event.data.id,
        email: event.data.email_addresses[0]?.email_address,
        name: `${event.data.first_name} ${event.data.last_name}`,
      });
      break;
    case 'user.updated':
      await db.users.update({ clerkId: event.data.id, ... });
      break;
    case 'user.deleted':
      await db.users.delete({ clerkId: event.data.id });
      break;
  }

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

Custom JWT Templates (for external services)

Configure in Clerk Dashboard under "JWT Templates":

{
  "sub": "{{user.id}}",
  "email": "{{user.primary_email_address}}",
  "name": "{{user.full_name}}",
  "org_id": "{{org.id}}",
  "org_role": "{{org_membership.role}}"
}
// Use the template to get a signed token for Supabase, Convex, Hasura, etc.
const token = await getToken({ template: 'supabase' });

Express Backend

import express from 'express';
import { clerkMiddleware, requireAuth, getAuth } from '@clerk/express';

const app = express();
app.use(clerkMiddleware());

// Public route
app.get('/', (req, res) => {
  res.json({ message: 'Public endpoint' });
});

// Protected route
app.get('/dashboard', requireAuth(), (req, res) => {
  const { userId } = getAuth(req);
  res.json({ userId });
});

app.listen(3001);

Best Practices

  • Use Clerk's pre-built <SignIn />, <SignUp />, and <UserButton /> components to save development time; customize with the appearance prop or Clerk themes.
  • Set up webhooks (user.created, user.updated, user.deleted) to keep your database synchronized with Clerk's user records.
  • Use JWT templates to issue tokens compatible with your backend (Supabase, Convex, Hasura) rather than passing Clerk tokens directly.
  • Protect routes at the middleware level using clerkMiddleware rather than checking auth in every page/route handler.
  • Use organizations for multi-tenant applications — Clerk handles invitations, roles, and membership out of the box.

Common Pitfalls

  • Not syncing users to your database: Clerk manages user data externally; if your app needs to store user-related data, set up webhooks to create local user records on user.created.
  • Exposing the secret key: CLERK_SECRET_KEY must never be in client-side code; use NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY for the browser.
  • Middleware matcher misconfiguration: If the middleware matcher is too broad, static assets and Next.js internals get intercepted; use the recommended matcher pattern.
  • Webhook verification skipped: Always verify webhook signatures using the svix library; unverified webhooks can be spoofed.
  • Token expiry in long-running operations: Clerk JWTs have a short lifespan (60 seconds by default); always call getToken() immediately before each API request rather than caching the token.

Anti-Patterns

Over-engineering for hypothetical requirements. Building for scenarios that may never materialize adds complexity without value. Solve the problem in front of you first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide wastes time and introduces risk.

Premature abstraction. Creating elaborate frameworks before having enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at system boundaries. Internal code can trust its inputs, but boundaries with external systems require defensive validation.

Skipping documentation. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add baas-skills

Get CLI access →