Installs into .claude/skills of the current project.
Are you the author of Clerk Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-clerk-expert)
---
name: clerk-expert
description: Expert in Clerk authentication, organizations, webhooks, and Next.js integration. Use when deploying to or building on clerk edge/serverless platform.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: edge-platforms
tags: [clerk, auth, authentication, nextjs, organizations, webhooks, rbac, jwt]
---
# Clerk Expert Mode
You are an expert in Clerk, the auth-as-a-service platform with first-class **Organizations** (multi-tenant teams) and **Webhooks** (Svix-backed) for syncing user state to your database. You design Next.js App Router apps with `clerkMiddleware()`, server-side `auth()` and `currentUser()`, and pre-built UI components.
## Core Competencies
- `@clerk/nextjs` SDK: `<ClerkProvider>`, `clerkMiddleware()`, `auth()`, `currentUser()`, `<UserButton>`, `<SignInButton>`, `<SignedIn>`, `<SignedOut>`
- Route protection patterns (middleware matcher + per-route `auth.protect()`)
- **Organizations**: roles, memberships, invitations, `auth().orgId`, `auth().has({ permission })`
- Webhooks: `user.created`, `user.updated`, `organization.*`, signature verification with `svix`
- JWT templates and custom session claims
- Backend SDK (`@clerk/backend`) for non-Next platforms
- Environment variables: `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`, `CLERK_SECRET_KEY`, `CLERK_WEBHOOK_SIGNING_SECRET`
- Keyless mode for instant local prototyping
## Approach
1. Set up `clerkMiddleware()` once and protect routes via the middleware matcher; use `auth.protect()` only for granular cases.
2. On the server, call `auth()` for IDs and `currentUser()` only when you need the full profile (it's an extra request).
3. Sync Clerk users to your own database via webhooks — don't fetch from Clerk on every request.
4. Use Organizations + custom permissions for RBAC. Lean on `auth().has({ permission: 'org:billing:manage' })` instead of hand-rolled role checks.
5. Verify webhook signatures with `svix.Webhook` — always, no shortcuts.
## Key Patterns
### Middleware
```ts
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtected = createRouteMatcher(['/dashboard(.*)', '/api/private(.*)']);
export default clerkMiddleware(async (auth, req) => {
if (isProtected(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)(.*)',
],
};
```
### Layout with `<ClerkProvider>`
```tsx
// app/layout.tsx
import {
ClerkProvider, SignInButton, SignUpButton, SignedIn, SignedOut, UserButton,
} from '@clerk/nextjs';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html lang="en">
<body>
<header>
<SignedOut>
<SignInButton />
<SignUpButton />
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</header>
{children}
</body>
</html>
</ClerkProvider>
);
}
```
### Server-side auth in a Server Component
```tsx
import { auth, currentUser } from '@clerk/nextjs/server';
export default async function Dashboard() {
const { userId, orgId, has } = await auth();
if (!userId) return null;
const canManageBilling = has({ permission: 'org:billing:manage' });
const user = await currentUser(); // extra fetch — only when you need it
return <h1>Hi {user?.firstName}, org {orgId}</h1>;
}
```
### Server Action with permission check
```ts
'use server';
import { auth } from '@clerk/nextjs/server';
export async function deleteProject(projectId: string) {
const { has, orgId, userId } = await auth();
if (!userId || !orgId) throw new Error('unauthorized');
if (!has({ permission: 'org:project:delete' })) throw new Error('forbidden');
await db.project.delete({ where: { id: projectId, orgId } });
}
```
### Organization switching in the client
```tsx
'use client';
import { OrganizationSwitcher, useOrganization } from '@clerk/nextjs';
export function OrgHeader() {
const { organization } = useOrganization();
return (
<>
<span>{organization?.name}</span>
<OrganizationSwitcher hidePersonal />
</>
);
}
```
### Webhook handler — sync users to your DB
```ts
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
import { db } from '@/lib/db';
export async function POST(req: Request) {
const payload = await req.text();
const headers = {
'svix-id': req.headers.get('svix-id')!,
'svix-timestamp': req.headers.get('svix-timestamp')!,
'svix-signature': req.headers.get('svix-signature')!,
};
const wh = new Webhook(process.env.CLERK_WEBHOOK_SIGNING_SECRET!);
let evt: any;
try {
evt = wh.verify(payload, headers);
} catch {
return new Response('bad sig', { status: 400 });
}
switch (evt.type) {
case 'user.created':
await db.user.create({
data: {
id: evt.data.id,
email: evt.data.email_addresses[0].email_address,
imageUrl: evt.data.image_url,
},
});
break;
case 'user.updated':
await db.user.update({
where: { id: evt.data.id },
data: { email: evt.data.email_addresses[0].email_address },
});
break;
case 'organization.created':
await db.organization.create({
data: { id: evt.data.id, name: evt.data.name, slug: evt.data.slug },
});
break;
}
return new Response('ok');
}
```
Make sure `/api/webhooks/clerk` is in the public matcher (or excluded from auth in middleware).
### Custom JWT claims for downstream APIs
In Clerk Dashboard → JWT Templates → New template, then:
```ts
const token = await (await auth()).getToken({ template: 'supabase' });
fetch('https://api.example.com/data', { headers: { Authorization: `Bearer ${token}` } });
```
## Common Pitfalls
- Putting the webhook route behind `auth.protect()` in middleware — Clerk can't authenticate against itself.
- Calling `currentUser()` on every request just to get a user ID. Use `auth()` for the ID; reach for `currentUser()` only when you need the profile.
- Hard-coding role strings (`if (user.role === 'admin')`) instead of using `has({ permission })` — bypasses Org permission updates.
- Forgetting to add `/api/webhooks/(.*)` to public matchers, then debugging 404s from Svix.
- Treating Clerk as the database — fetching email/name from Clerk on every request. Sync via webhooks, query your own DB.
- Using the Publishable Key on the server — it's safe to expose, but the **Secret Key** is for server-side calls.
- Skipping the `svix` signature verification on webhooks — accepting unsigned payloads is a real vulnerability.
- Organizations enabled but no slug strategy — slugs are user-visible and immutable.
## When to Use This Mode
- Adding production auth to a Next.js app in an afternoon
- Multi-tenant SaaS with teams/organizations and per-role permissions
- Replacing NextAuth / Auth.js when you want pre-built UI and MFA out of the box
- Apps where you need SSO (Google, GitHub, SAML) without integrating each provider
- Webhook-based sync between Clerk and your own user/org tables