Skills clerk-auth
install
source · Clone the upstream repo
git clone https://github.com/TerminalSkills/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/TerminalSkills/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/clerk-auth" ~/.claude/skills/terminalskills-skills-clerk-auth && rm -rf "$T"
manifest:
skills/clerk-auth/SKILL.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- references .env files
- references API keys
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
Clerk Authentication
Drop-in authentication for modern web apps. Handles login UI, social providers, session management, organizations, and RBAC.
Setup (Next.js)
npm install @clerk/nextjs
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_... CLERK_SECRET_KEY=sk_live_...
// 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> ); }
Middleware (Route Protection)
// 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)(.*)'], };
Server-Side Auth
Server Components (App Router)
import { 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>; }
API Routes
import { 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 }
Server Actions
'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 } }); }
Client-Side Auth
'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> ); }
Pre-Built Components
import { 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> ); }
Organizations (Multi-Tenant)
Enable at dashboard.clerk.com → Organizations.
Create Organization
import { 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!, }); }
Invite Members
async 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!, }); }
Custom Roles
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:
const { 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' })) { ... }
Webhooks
Sync Clerk events to your database:
// 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.
JWT Templates (API Auth)
For external APIs or microservices that need to verify Clerk tokens:
// 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); }
Express.js
import { 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 }); });
Guidelines
- Middleware is the primary protection layer — don't rely on component-level checks alone. Middleware runs at the edge before any page code.
- Use
in server components, notauth()
— server-side checks can't be bypassed by the clientuseAuth() - Webhook signature verification is mandatory — use
library to verify every webhook payloadsvix - Sync to your database via webhooks — don't query Clerk's API for every database operation. Keep a local copy of users and orgs.
- Use organizations for B2B — even if you think you only need simple auth now. Adding multi-tenancy later is much harder than starting with it.
- Permission-based checks over role checks —
is more maintainable thanhas({ permission: 'X' })role === 'org:admin'
for B2B apps — personal workspaces confuse users in team-based productshidePersonal={true}- Configure sign-in/up URLs in env vars — Clerk uses these for redirects after auth flows