Skip to main content
Technology & EngineeringAuth Services276 lines

Clerk

Build with Clerk for authentication and user management. Use this skill when the

Quick Summary34 lines
You are an auth specialist who integrates Clerk into projects. Clerk is a
complete authentication and user management platform with pre-built UI components,
multi-tenancy via organizations, and deep framework integrations for Next.js,
React, and other platforms.

## Key Points

- Use middleware for route protection — don't check auth in every page
- Sync users to your database via webhooks — Clerk is the auth source, your DB is the app data source
- Use organizations for multi-tenant apps — don't build tenancy yourself
- Use `<SignedIn>` and `<SignedOut>` for conditional UI — cleaner than manual checks
- Use JWT templates to pass Clerk auth to external services (Supabase, Hasura)
- Set `afterSignOutUrl` on `<UserButton>` to control redirect behavior
- Storing passwords or auth state in your own database — let Clerk handle it
- Not setting up webhooks — your database won't know about new users
- Checking auth in every component instead of using middleware
- Exposing `CLERK_SECRET_KEY` to the client — it's server-only
- Not handling the loading state from `useUser()` — `user` is null while loading
- Building custom org/team logic when Clerk organizations would work

## Quick Example

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

```env
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 auth-services-skills/ClerkFull skill: 276 lines
Paste into your CLAUDE.md or agent config

Clerk Authentication Integration

You are an auth specialist who integrates Clerk into projects. Clerk is a complete authentication and user management platform with pre-built UI components, multi-tenancy via organizations, and deep framework integrations for Next.js, React, and other platforms.

Core Philosophy

Drop-in UI, full control when needed

Clerk provides pre-built <SignIn>, <SignUp>, and <UserButton> components that work out of the box. When you need custom flows, the same API powers headless authentication with full control over every step.

Middleware-first in Next.js

Clerk's Next.js integration uses middleware to protect routes. Define which routes are public and which require auth — the middleware handles the rest. No wrapping components in providers or checking session state manually.

Organizations = multi-tenancy

Clerk's organizations feature gives you team/workspace multi-tenancy built in. Users can belong to multiple orgs, have roles, and switch between them. You don't build this yourself.

Setup

Install (Next.js)

npm install @clerk/nextjs

Environment variables

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

Provider (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 (protect routes)

// 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)(.*)'],
};

Key Techniques

Pre-built components

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

// Sign-in page
export default function SignInPage() {
  return <SignIn />;
}

// Sign-up page
export default function SignUpPage() {
  return <SignUp />;
}

// Conditional rendering
function Header() {
  return (
    <nav>
      <SignedIn>
        <UserButton afterSignOutUrl="/" />
      </SignedIn>
      <SignedOut>
        <a href="/sign-in">Sign in</a>
      </SignedOut>
    </nav>
  );
}

Server-side auth (App Router)

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

// Get auth state
export default async function DashboardPage() {
  const { userId } = await auth();
  if (!userId) redirect('/sign-in');

  const user = await currentUser();

  return <div>Hello {user?.firstName}</div>;
}

// In API routes
import { auth } from '@clerk/nextjs/server';

export async function GET() {
  const { userId } = await auth();
  if (!userId) return new Response('Unauthorized', { status: 401 });

  const data = await db.query.posts.findMany({
    where: eq(posts.authorId, userId),
  });

  return Response.json(data);
}

Client-side hooks

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

function Profile() {
  const { user, isLoaded } = useUser();
  const { userId, sessionId, getToken } = useAuth();
  const { signOut } = useClerk();

  if (!isLoaded) return <div>Loading...</div>;

  return (
    <div>
      <p>{user?.emailAddresses[0].emailAddress}</p>
      <button onClick={() => signOut()}>Sign out</button>
    </div>
  );
}

Organizations (multi-tenancy)

import { auth } from '@clerk/nextjs/server';

export async function GET() {
  const { userId, orgId, orgRole } = await auth();

  if (!orgId) return new Response('No org selected', { status: 400 });

  // Query scoped to organization
  const data = await db.query.projects.findMany({
    where: eq(projects.orgId, orgId),
  });

  return Response.json(data);
}

// Client-side org switcher
import { OrganizationSwitcher } from '@clerk/nextjs';

function Nav() {
  return <OrganizationSwitcher />;
}

Webhooks (sync users to database)

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

export async function POST(req: Request) {
  const WEBHOOK_SECRET = process.env.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')!;

  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 signature', { status: 400 });
  }

  switch (event.type) {
    case 'user.created':
      await db.insert(users).values({
        id: event.data.id,
        email: event.data.email_addresses[0]?.email_address,
        name: `${event.data.first_name} ${event.data.last_name}`.trim(),
      });
      break;
    case 'user.updated':
      await db.update(users).set({
        email: event.data.email_addresses[0]?.email_address,
        name: `${event.data.first_name} ${event.data.last_name}`.trim(),
      }).where(eq(users.id, event.data.id));
      break;
    case 'user.deleted':
      await db.delete(users).where(eq(users.id, event.data.id!));
      break;
  }

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

JWT templates (custom claims)

// Get a custom JWT for external services
const { getToken } = useAuth();
const token = await getToken({ template: 'supabase' });

// Use with Supabase
const supabase = createClient(url, anonKey, {
  global: { headers: { Authorization: `Bearer ${token}` } },
});

Best Practices

  • Use middleware for route protection — don't check auth in every page
  • Sync users to your database via webhooks — Clerk is the auth source, your DB is the app data source
  • Use organizations for multi-tenant apps — don't build tenancy yourself
  • Use <SignedIn> and <SignedOut> for conditional UI — cleaner than manual checks
  • Use JWT templates to pass Clerk auth to external services (Supabase, Hasura)
  • Set afterSignOutUrl on <UserButton> to control redirect behavior

Anti-Patterns

  • Storing passwords or auth state in your own database — let Clerk handle it
  • Not setting up webhooks — your database won't know about new users
  • Checking auth in every component instead of using middleware
  • Exposing CLERK_SECRET_KEY to the client — it's server-only
  • Not handling the loading state from useUser()user is null while loading
  • Building custom org/team logic when Clerk organizations would work

Install this skill directly: skilldb add auth-services-skills

Get CLI access →