skills/clerk-auth/SKILL.md
Expert patterns for Clerk auth implementation, middleware, organizations, webhooks, and user sync
npx skillsauth add Regtransfers/agency-agents-mcp clerk-authInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
@ Clerk Authentication
Expert patterns for Clerk auth implementation, middleware, organizations, webhooks, and user sync
@ Patterns
@ Next.js App Router Setup
Complete Clerk setup for Next.js 14/15 App Router.
Includes ClerkProvider, environment variables, and basic sign-in/sign-up components.
Key components:
@ Code_example
@ Environment variables (.env.local) NEXTPUBLICCLERKPUBLISHABLEKEY=pktest... CLERKSECRETKEY=sktest... NEXTPUBLICCLERKSIGNIN_URL=/sign-in NEXTPUBLICCLERKSIGNUP_URL=/sign-up NEXTPUBLICCLERKAFTERSIGNINURL=/dashboard NEXTPUBLICCLERKAFTERSIGNUPURL=/onboarding
// 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> ); }// app/sign-in/[[...sign-in]]/page.tsx import { SignIn } from '@clerk/nextjs';
export default function SignInPage() { return (
<div className="flex justify-center items-center min-h-screen"> <SignIn /> </div> ); }// app/sign-up/[[...sign-up]]/page.tsx import { SignUp } from '@clerk/nextjs';
export default function SignUpPage() { return (
<div className="flex justify-center items-center min-h-screen"> <SignUp /> </div> ); }// components/Header.tsx import { SignedIn, SignedOut, SignInButton, UserButton } from '@clerk/nextjs';
export function Header() { return (
<header className="flex justify-between p-4"> <h1>My App</h1> <SignedOut> <SignInButton /> </SignedOut> <SignedIn> <UserButton afterSignOutUrl="/" /> </SignedIn> </header> ); }@ Anti_patterns
@ References
@ Middleware Route Protection
Protect routes using clerkMiddleware and createRouteMatcher.
Best practices:
@ Code_example
// middleware.ts import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
// Define protected route patterns const isProtectedRoute = createRouteMatcher([ '/dashboard(.)', '/settings(.)', '/api/private(.*)', ]);
// Define public routes (optional, for clarity) const isPublicRoute = createRouteMatcher([ '/', '/sign-in(.)', '/sign-up(.)', '/api/webhooks(.*)', ]);
export default clerkMiddleware(async (auth, req) => { // Protect matched routes if (isProtectedRoute(req)) { await auth.protect(); } });
export const config = { matcher: [ // Match all routes except static files '/((?!_next; [^?]\.(?:html?; css; js(?!on); jpe?g; webp; png; gif; svg; ttf; woff2?; ico; csv; docx?; xlsx?; zip; webmanifest)).)', // Always run for API routes '/(api; trpc)(.*)', ], };
// Advanced: Role-based protection export default clerkMiddleware(async (auth, req) => { if (isProtectedRoute(req)) { await auth.protect(); }
// Admin routes require admin role if (req.nextUrl.pathname.startsWith('/admin')) { await auth.protect({ role: 'org:admin', }); }
// Premium routes require premium permission if (req.nextUrl.pathname.startsWith('/premium')) { await auth.protect({ permission: 'org:premium:access', }); } });
@ Anti_patterns
@ References
@ Server Component Authentication
Access auth state in Server Components using auth() and currentUser().
Key functions:
@ Code_example
// app/dashboard/page.tsx (Server Component) import { auth, currentUser } from '@clerk/nextjs/server'; import { redirect } from 'next/navigation';
export default async function DashboardPage() { const { userId } = await auth();
if (!userId) { redirect('/sign-in'); }
// Full user data (counts toward rate limits) const user = await currentUser();
return (
<div> <h1>Welcome, {user?.firstName}!</h1> <p>Email: {user?.emailAddresses[0]?.emailAddress}</p> </div> ); }// Using auth() for quick checks export default async function ProtectedLayout({ children, }: { children: React.ReactNode; }) { const { userId, orgId, orgRole } = await auth();
if (!userId) { redirect('/sign-in'); }
// Check organization access if (!orgId) { redirect('/select-org'); }
return (
<div> <p>Organization Role: {orgRole}</p> {children} </div> ); }// Server Action with auth check // app/actions/posts.ts 'use server'; import { auth } from '@clerk/nextjs/server';
export async function createPost(formData: FormData) { const { userId } = await auth();
if (!userId) { throw new Error('Unauthorized'); }
const title = formData.get('title') as string;
// Create post with userId const post = await prisma.post.create({ data: { title, authorId: userId, }, });
return post; }
@ Anti_patterns
@ References
@ Client Component Hooks
Access auth state in Client Components using hooks.
Key hooks:
@ Code_example
// components/UserProfile.tsx 'use client'; import { useUser, useAuth } from '@clerk/nextjs';
export function UserProfile() { const { user, isLoaded, isSignedIn } = useUser(); const { signOut } = useAuth();
if (!isLoaded) { return <div>Loading...</div>; }
if (!isSignedIn) { return <div>Not signed in</div>; }
return (
<div> <img src={user.imageUrl} alt={user.fullName?? ''} /> <h2>{user.fullName}</h2> <p>{user.emailAddresses[0]?.emailAddress}</p> <button onClick={() => signOut()}>Sign Out</button> </div> ); }// Organization context 'use client'; import { useOrganization, useOrganizationList } from '@clerk/nextjs';
export function OrgSwitcher() { const { organization, membership } = useOrganization(); const { setActive, userMemberships } = useOrganizationList({ userMemberships: { infinite: true }, });
if (!organization) { return <p>No organization selected</p>; }
return (
<div> <p>Current: {organization.name}</p> <p>Role: {membership?.role}</p><select onChange={(e) => setActive?.({ organization: e.target.value })} value={organization.id}
{userMemberships.data?.map((mem) => (
<option key={mem.organization.id} value={mem.organization.id}> {mem.organization.name} </option> ))} </select> </div> ); }// Protected client component 'use client'; import { useAuth } from '@clerk/nextjs'; import { useRouter } from 'next/navigation'; import { useEffect } from 'react';
export function ProtectedContent() { const { isLoaded, userId } = useAuth(); const router = useRouter();
useEffect(() => { if (isLoaded &&!userId) { router.push('/sign-in'); } }, [isLoaded, userId, router]);
if (!isLoaded;!userId) { return <div>Loading...</div>; }
return <div>Protected content here</div>; }
@ Anti_patterns
@ References
@ Organizations and Multi-Tenancy
Implement B2B multi-tenancy with Clerk Organizations.
Features:
@ Code_example
// Organization creation UI // app/create-org/page.tsx import { CreateOrganization } from '@clerk/nextjs';
export default function CreateOrgPage() { return (
<div className="flex justify-center"> <CreateOrganization afterCreateOrganizationUrl="/dashboard" /> </div> ); }// Organization profile and management // app/org-settings/page.tsx import { OrganizationProfile } from '@clerk/nextjs';
export default function OrgSettingsPage() { return <OrganizationProfile />; }
// Organization switcher in header // components/Header.tsx import { OrganizationSwitcher, UserButton } from '@clerk/nextjs';
export function Header() { return (
<header className="flex justify-between p-4"> <OrganizationSwitcher hidePersonal afterCreateOrganizationUrl="/dashboard" afterSelectOrganizationUrl="/dashboard" /> <UserButton /> </header> ); }// Org-scoped data access // app/dashboard/page.tsx import { auth } from '@clerk/nextjs/server'; import { prisma } from '@/lib/prisma';
export default async function DashboardPage() { const { orgId } = await auth();
if (!orgId) { redirect('/select-org'); }
// Fetch org-scoped data const projects = await prisma.project.findMany({ where: { organizationId: orgId }, });
return (
<div> <h1>Projects</h1> {projects.map((p) => ( <div key={p.id}>{p.name}</div> ))} </div> ); }// Role-based UI 'use client'; import { useOrganization, Protect } from '@clerk/nextjs';
export function AdminPanel() { const { membership } = useOrganization();
// Using Protect component return ( <Protect role="org:admin" fallback={<p>Admin access required</p>}>
<div>Admin content here</div> </Protect> );// Or manual check if (membership?.role!== 'org:admin') { return <p>Admin access required</p>; }
return <div>Admin content here</div>; }
@ Anti_patterns
@ References
@ Webhook User Sync
Sync Clerk users to your database using webhooks.
Key webhooks:
Uses svix for signature verification.
@ Code_example
// app/api/webhooks/clerk/route.ts import { Webhook } from 'svix'; import { headers } from 'next/headers'; import { WebhookEvent } from '@clerk/nextjs/server'; import { prisma } from '@/lib/prisma';
export async function POST(req: Request) { const WEBHOOKSECRET = process.env.CLERKWEBHOOK_SECRET;
if (!WEBHOOK_SECRET) { throw new Error('Missing CLERKWEBHOOKSECRET'); }
// Get headers 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');
if (!svixid;!svixtimestamp;!svix_signature) { return new Response('Missing svix headers', { status: 400 }); }
// Get body const payload = await req.json(); const body = JSON.stringify(payload);
// Verify webhook const wh = new Webhook(WEBHOOK_SECRET); let evt: WebhookEvent;
try { evt = wh.verify(body, { 'svix-id': svix_id, 'svix-timestamp': svix_timestamp, 'svix-signature': svix_signature, }) as WebhookEvent; } catch (err) { console.error('Webhook verification failed:', err); return new Response('Verification failed', { status: 400 }); }
// Handle events const eventType = evt.type;
if (eventType === 'user.created') { const { id, emailaddresses, firstname, lastname, imageurl } = evt.data;
await prisma.user.create({ data: { clerkId: id, email: emailaddresses[0]?.emailaddress, firstName: first_name, lastName: last_name, imageUrl: image_url, }, }); }
if (eventType === 'user.updated') { const { id, emailaddresses, firstname, lastname, imageurl } = evt.data;
await prisma.user.update({ where: { clerkId: id }, data: { email: emailaddresses[0]?.emailaddress, firstName: first_name, lastName: last_name, imageUrl: image_url, }, }); }
if (eventType === 'user.deleted') { const { id } = evt.data;
await prisma.user.delete({ where: { clerkId: id! }, }); }
return new Response('Webhook processed', { status: 200 }); }
// Prisma schema // prisma/schema.prisma model User { id String @id @default(cuid()) clerkId String @unique email String @unique firstName String? lastName String? imageUrl String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
posts Post[] @@index([clerkId]) }
@ Anti_patterns
@ References
@ API Route Protection
Protect API routes using auth() from Clerk.
Route Handlers in App Router use auth() for authentication. Middleware provides initial protection, auth() provides in-handler verification.
@ Code_example
// app/api/projects/route.ts import { auth } from '@clerk/nextjs/server'; import { prisma } from '@/lib/prisma'; import { NextResponse } from 'next/server';
export async function GET() { const { userId, orgId } = await auth();
if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }
// User's personal projects or org projects const projects = await prisma.project.findMany({ where: orgId ? { organizationId: orgId } : { userId, organizationId: null }, });
return NextResponse.json(projects); }
export async function POST(req: Request) { const { userId, orgId } = await auth();
if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }
const body = await req.json();
const project = await prisma.project.create({ data: { name: body.name, userId, organizationId: orgId?? null, }, });
return NextResponse.json(project, { status: 201 }); }
// Protected with role check // app/api/admin/users/route.ts export async function GET() { const { userId, orgRole } = await auth();
if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }
if (orgRole!== 'org:admin') { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); }
// Admin-only logic const users = await prisma.user.findMany(); return NextResponse.json(users); }
// Using getAuth in older patterns (not recommended) // For backwards compatibility only import { getAuth } from '@clerk/nextjs/server';
export async function GET(req: Request) { const { userId } = getAuth(req); //... }
@ Anti_patterns
@ References
@ Sharp Edges
@ CVE-2025-29927 Middleware Bypass Vulnerability
Severity: CRITICAL
@ Multiple Middleware Files Cause Conflicts
Severity: HIGH
@ 4KB Session Token Cookie Limit
Severity: HIGH
@ auth() Requires clerkMiddleware Configuration
Severity: HIGH
@ Webhook Race Conditions
Severity: MEDIUM
@ auth() is Async in App Router
Severity: MEDIUM
@ Middleware Blocks Webhook Endpoints
Severity: MEDIUM
@ Accessing Auth State Before isLoaded
Severity: MEDIUM
@ Manual Redirects Cause Double Redirects
Severity: MEDIUM
@ Organization Data Not Scoped by orgId
Severity: HIGH
@ Validation Checks
@ Clerk Secret Key in Client Code
Severity: ERROR
CLERKSECRETKEY must only be used server-side
Message: Clerk secret key exposed to client. Use CLERKSECRETKEY without NEXT_PUBLIC prefix.
@ Protected Route Without Middleware
Severity: ERROR
API routes should have middleware protection
Message: API route without auth check. Add middleware protection or auth() check.
@ Hardcoded Clerk API Keys
Severity: ERROR
Clerk keys should use environment variables
Message: Hardcoded Clerk keys. Use environment variables.
@ Missing Await on auth()
Severity: ERROR
auth() is async in App Router and must be awaited
Message: auth() not awaited. Use 'await auth()' in App Router.
@ Multiple Middleware Files
Severity: WARNING
Only one middleware.ts file should exist
Message: Multiple middleware files detected. Use single middleware.ts.
@ Webhook Route Not Excluded from Protection
Severity: WARNING
Webhook routes should be public
Message: Webhook route may be blocked by middleware. Add to public routes.
@ Accessing Auth Without isLoaded Check
Severity: WARNING
Check isLoaded before accessing user state in client components
Message: Accessing user without isLoaded check. Check isLoaded first.
@ Clerk Hooks in Server Component
Severity: ERROR
Clerk hooks only work in Client Components
Message: Clerk hooks in Server Component. Add 'use client' or use auth().
@ Multi-Tenant Query Without orgId
Severity: WARNING
Organization data should be scoped by orgId
Message: Query without organization scope. Filter by orgId for multi-tenancy.
@ Webhook Without Signature Verification
Severity: ERROR
Clerk webhooks must verify svix signature
Message: Webhook without signature verification. Use svix to verify.
@ Collaboration
@ Delegation Triggers
@ When to Use
@ Limitations
tools
Build AI agents that interact with computers like humans do - viewing screens, moving cursors, clicking buttons, and typing text. Covers Anthropic's Computer Use, OpenAI's Operator/CUA, and open-source alternatives.
testing
Generate structured PR descriptions from diffs, add review checklists, risk assessments, and test coverage summaries. Use when the user says "write a PR description", "improve this PR", "summarize my changes", "PR review", "pull request", or asks to document a diff for reviewers.
tools
Use when working with comprehensive review full review
development
You are an expert in creating competitor comparison and alternative pages. Your goal is to build pages that rank for competitive search terms, provide genuine value to evaluators, and position your product effectively.