Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Add authentication to web apps with Clerk — social login, email/password, magic links, organizations, RBAC, session management, webhooks, and multi-framework support. Use when tasks involve user authentication, team/org management, role-based access control, or integrating auth into Next.js, React, Remix, or Express applications.
.claude/skills/terminalskills-clerk-auth/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | 72% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 35% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 60% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 21% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 114% | 0% |
Drop-in authentication for modern web apps. Handles login UI, social providers, session management, organizations, and RBAC.
bashnpm install @clerk/nextjs
envNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_... CLERK_SECRET_KEY=sk_live_...
typescript// app/layout.tsx — Wrap app in ClerkProvider import { ClerkProvider } from '@clerk/nextjs'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <ClerkProvider> <html><body>{children}</body></html> </ClerkProvider> ); }
typescript// middleware.ts — Protect routes at the edge import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; const isPublicRoute = createRouteMatcher([ '/', '/pricing', '/sign-in(.*)', '/sign-up(.*)', '/api/webhooks(.*)', ]); export default clerkMiddleware(async (auth, req) => { if (!isPublicRoute(req)) { await auth.protect(); } }); export const config = { matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], };
typescriptimport { auth, currentUser } from '@clerk/nextjs/server'; export default async function Page() { // Quick access to IDs and role const { userId, orgId, orgRole } = await auth(); // Full user object when needed const user = await currentUser(); return <p>Hello {user?.firstName}</p>; }
typescriptimport { auth } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server'; export async function GET() { const { userId, orgId } = await auth(); if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } // ... fetch data scoped to orgId }
typescript'use server'; import { auth } from '@clerk/nextjs/server'; export async function createProject(name: string) { const { userId, orgId, orgRole } = await auth(); if (!orgId || (orgRole !== 'org:admin' && orgRole !== 'org:owner')) { throw new Error('Forbidden'); } return db.projects.create({ data: { name, orgId, createdBy: userId } }); }
typescript'use client'; import { useAuth, useUser, useOrganization } from '@clerk/nextjs'; export function ProfileCard() { const { isSignedIn, userId } = useAuth(); const { user } = useUser(); const { organization, membership } = useOrganization(); if (!isSignedIn) return <p>Not signed in</p>; return ( <div> <p>{user?.fullName}</p> <p>Org: {organization?.name}</p> <p>Role: {membership?.role}</p> </div> ); }
typescriptimport { SignIn, // Full sign-in page SignUp, // Full sign-up page UserButton, // Avatar dropdown (profile, sign out) UserProfile, // Full profile management page OrganizationSwitcher, // Org dropdown + create org OrganizationList, // List orgs + join/create OrganizationProfile, // Org settings (members, roles) } from '@clerk/nextjs'; // Sign-in page // app/sign-in/[[...sign-in]]/page.tsx export default function SignInPage() { return <SignIn />; } // Header with org switcher and user menu export function Header() { return ( <nav> <OrganizationSwitcher hidePersonal={true} /> <UserButton afterSignOutUrl="/" /> </nav> ); }
Enable at dashboard.clerk.com → Organizations.
typescriptimport { auth, clerkClient } from '@clerk/nextjs/server'; async function createOrg(name: string) { const { userId } = await auth(); const client = await clerkClient(); return client.organizations.createOrganization({ name, createdBy: userId!, }); }
typescriptasync function inviteMember(orgId: string, email: string, role: string) { const client = await clerkClient(); return client.organizations.createOrganizationInvitation({ organizationId: orgId, emailAddress: email, role, // 'org:admin', 'org:member', or custom roles inviterUserId: (await auth()).userId!, }); }
Define at dashboard.clerk.com → Organizations → Roles:
org:owner — Full access, can delete org
org:admin — Manage members, settings
org:member — Standard access
org:viewer — Read-only (custom)
org:billing — Billing management only (custom)Check roles in code:
typescriptconst { orgRole, has } = await auth(); // Direct role check if (orgRole === 'org:admin') { ... } // Permission-based check (preferred — decouples code from role names) if (has({ permission: 'org:projects:manage' })) { ... }
Sync Clerk events to your database:
typescript// app/api/webhooks/clerk/route.ts import { Webhook } from 'svix'; import { WebhookEvent } from '@clerk/nextjs/server'; export async function POST(req: Request) { const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!); const body = await req.text(); const svixHeaders = { 'svix-id': req.headers.get('svix-id')!, 'svix-timestamp': req.headers.get('svix-timestamp')!, 'svix-signature': req.headers.get('svix-signature')!, }; const event = wh.verify(body, svixHeaders) as WebhookEvent; switch (event.type) { case 'user.created': await db.users.create({ data: { clerkId: event.data.id, email: event.data.email_addresses[0]?.email_address, name: `${event.data.first_name} ${event.data.last_name}`.trim(), }}); break; case 'user.deleted': await db.users.delete({ where: { clerkId: event.data.id } }); break; case 'organization.created': await db.orgs.create({ data: { clerkOrgId: event.data.id, name: event.data.name, slug: event.data.slug, }}); break; } return new Response('OK'); }
Key events: user.created, user.updated, user.deleted, organization.created, organization.updated, organizationMembership.created, organizationMembership.deleted.
For external APIs or microservices that need to verify Clerk tokens:
typescript// Configure at dashboard.clerk.com → JWT Templates // Template name: "api-token" // Claims: { "userId": "{{user.id}}", "orgId": "{{org.id}}", "role": "{{org.role}}" } // Client: get a custom JWT const { getToken } = useAuth(); const token = await getToken({ template: 'api-token' }); // External API: verify the JWT import { createClerkClient } from '@clerk/backend'; const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY }); async function verifyRequest(req: Request) { const token = req.headers.get('Authorization')?.replace('Bearer ', ''); if (!token) throw new Error('No token'); return clerk.verifyToken(token); }
typescriptimport { ClerkExpressRequireAuth } from '@clerk/clerk-sdk-node'; // Protect routes app.use('/api', ClerkExpressRequireAuth()); app.get('/api/me', (req, res) => { res.json({ userId: req.auth.userId, orgId: req.auth.orgId }); });
auth() in server components, not useAuth() — server-side checks can't be bypassed by the clientsvix library to verify every webhook payloadhas({ permission: 'X' }) is more maintainable than role === 'org:admin'hidePersonal={true} for B2B apps — personal workspaces confuse users in team-based products| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | fail→pass | 10,056 | 6,383 | -37% | 1 | 1 | 0% | 2,110 | 3,623 | +72% | 0 | 0 | — |
case-01 | pass→pass | 8,357 | 9,749 | +17% | 1 | 1 | 0% | 1,743 | 3,706 | +113% | 0 | 0 | — |
case-02 | fail→pass | 11,869 | 5,266 | -56% | 1 | 1 | 0% | 2,556 | 3,451 | +35% | 0 | 0 | — |
case-03 | pass→pass | 11,708 | 5,038 | -57% | 1 | 1 | 0% | 2,338 | 3,345 | +43% | 0 | 0 | — |
case-04 | fail→pass | 9,240 | 4,283 | -54% | 1 | 1 | 0% | 1,889 | 3,018 | +60% | 0 | 0 | — |
case-11 | fail→pass | 16,326 | 8,974 | -45% | 1 | 1 | 0% | 3,556 | 4,301 | +21% | 0 | 0 | — |
case-05 | fail→pass | 9,174 | 3,482 | -62% | 1 | 1 | 0% | 1,397 | 2,984 | +114% | 0 | 0 | — |
case-06 | fail→fail | 11,410 | 6,113 | -46% | 1 | 1 | 0% | 2,800 | 3,589 | +28% | 0 | 0 | — |
case-07 | pass→pass | 12,859 | 5,659 | -56% | 1 | 1 | 0% | 2,872 | 3,604 | +25% | 0 | 0 | — |
case-08 | fail→pass | 18,750 | 6,113 | -67% | 1 | 1 | 0% | 4,301 | 3,643 | -15% | 0 | 0 | — |
case-09 | pass→pass | 11,447 | 5,071 | -56% | 1 | 1 | 0% | 2,414 | 3,339 | +38% | 0 | 0 | — |
case-12 | pass→pass | 10,142 | 9,240 | -9% | 1 | 1 | 0% | 2,365 | 4,585 | +94% | 0 | 0 | — |
case-13 | pass→pass | 11,344 | 9,267 | -18% | 1 | 1 | 0% | 2,409 | 3,915 | +63% | 0 | 0 | — |
case-14 | fail→pass | 11,966 | 5,891 | -51% | 1 | 1 | 0% | 1,950 | 3,709 | +90% | 0 | 0 | — |
case-15 | fail→fail | 11,165 | 7,521 | -33% | 1 | 1 | 0% | 2,234 | 3,976 | +78% | 0 | 0 | — |
case-16 | fail→fail | 10,557 | 6,701 | -37% | 1 | 1 | 0% | 2,123 | 3,765 | +77% | 0 | 0 | — |
case-17 | pass→pass | 5,604 | 7,161 | +28% | 1 | 1 | 0% | 1,236 | 2,893 | +134% | 0 | 0 | — |
case-18 | pass→pass | 7,316 | 3,182 | -57% | 1 | 1 | 0% | 1,633 | 2,907 | +78% | 0 | 0 | — |
case-19 | pass→pass | 6,798 | 3,480 | -49% | 1 | 1 | 0% | 1,397 | 2,934 | +110% | 0 | 0 | — |
case-20 | pass→pass | 9,219 | 10,829 | +17% | 1 | 1 | 0% | 1,899 | 4,131 | +118% | 0 | 0 | — |
case-21 | pass→pass | 7,097 | 4,092 | -42% | 1 | 1 | 0% | 811 | 3,106 | +283% | 0 | 0 | — |
case-22 | pass→pass | 11,109 | 8,253 | -26% | 1 | 1 | 0% | 2,320 | 3,871 | +67% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.