clerk-auth

$npx mdskill add TerminalSkills/skills/clerk-auth

Adds authentication to web apps using Clerk with RBAC and multi-framework support

  • Solves user authentication, team management, and role-based access control needs
  • Uses Clerk APIs for social login, email/password, and session management
  • Enforces route protection and permissions via middleware and providers
  • Delivers ready-to-use auth components and hooks for Next.js, React, and Express
SKILL.md
.github/skills/clerk-authView on GitHub ↗
---
name: clerk-auth
description: >-
  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.
license: Apache-2.0
compatibility: "Requires Node.js 16+"
metadata:
  author: terminal-skills
  version: "1.0.0"
  category: development
  tags: ["clerk", "authentication", "nextjs", "react", "rbac"]
---

# Clerk Authentication

Drop-in authentication for modern web apps. Handles login UI, social providers, session management, organizations, and RBAC.

## Setup (Next.js)

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

```env
NEXT_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>
  );
}
```

## Middleware (Route Protection)

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

## Server-Side Auth

### Server Components (App Router)

```typescript
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

```typescript
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

```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 } });
}
```

## Client-Side Auth

```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>
  );
}
```

## Pre-Built Components

```typescript
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

```typescript
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

```typescript
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:

```typescript
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:

```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`.

## JWT Templates (API Auth)

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);
}
```

## Express.js

```typescript
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 `auth()` in server components, not `useAuth()`** — server-side checks can't be bypassed by the client
- **Webhook signature verification is mandatory** — use `svix` library to verify every webhook payload
- **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** — `has({ permission: 'X' })` is more maintainable than `role === 'org:admin'`
- **`hidePersonal={true}` for B2B apps** — personal workspaces confuse users in team-based products
- **Configure sign-in/up URLs in env vars** — Clerk uses these for redirects after auth flows
More from TerminalSkills/skills