Skip to content
Back to skills

Clerk Auth

ASecurity

Clerk is a hosted authentication and user-management service with drop-in UI components, social login, email/password, organizations, RBAC and webhooks. Use when adding sign-in to Next.js, React, or Express apps, protecting routes with clerkMiddleware, setting up multi-tenant organizations and roles, or syncing Clerk users to your own database.

  • 142 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 27, 2026
developmentjavascripttypescriptgojavabashreactnextjsnodeexpressgit

Works with

  • terminal
  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add TerminalSkills/skills --skill clerk-auth --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Clerk Auth?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Clerk Auth
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/terminalskills-clerk-auth/badge)](https://www.skillsdirectory.com/skills/terminalskills-clerk-auth)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: clerk-auth
description: >-
  Clerk is a hosted authentication and user-management service with drop-in UI
  components, social login, email/password, organizations, RBAC and webhooks.
  Use when adding sign-in to Next.js, React, or Express apps, protecting routes
  with clerkMiddleware, setting up multi-tenant organizations and roles, or
  syncing Clerk users to your own database.
license: Apache-2.0
compatibility: "Node.js 20.9+; @clerk/nextjs 7 (Core 3) needs Next.js 15.2.3+ (proxy.ts on Next.js 16, middleware.ts on 15); a Clerk account and application"
metadata:
  author: terminal-skills
  version: "1.2.0"
  category: development
  tags: ["clerk", "authentication", "nextjs", "react", "rbac"]
  repository: https://github.com/clerk/javascript
---

# Clerk Authentication

## Overview

Clerk provides hosted sign-in and sign-up, session management, user profiles, organizations (multi-tenancy) and role-based access control. Your app wraps itself in a provider, adds a middleware that attaches the session, checks it next to the data, and reads the session through `auth()` on the server or hooks on the client. Keys come from the Clerk dashboard (Configure, API keys). Use the `pk_test_`/`sk_test_` keys for development and the `pk_live_`/`sk_live_` pair only in production. Checked against `@clerk/nextjs` 7.9 and `@clerk/express` 2.1 (Clerk Core 3). Core 3 replaced `<SignedIn>`, `<SignedOut>` and `<Protect>` with `<Show>`, requires `ClerkProvider` inside `<body>`, and dropped Next.js 13 and 14.

## Instructions

### 1. Install and configure (Next.js App Router)

Clerk's own quickstart now starts from its CLI, which detects the framework, installs the SDK, writes dev keys to `.env.local` and creates the middleware file (no account needed to start; it provisions a claimable dev app): `npx -y clerk@latest init`, then `npx -y clerk@latest doctor` to check the setup. Manual setup:

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

```env
# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=   # publishable key from the Clerk dashboard (API keys)
CLERK_SECRET_KEY=                    # secret key from the same page; never commit it
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
```

```tsx
// app/layout.tsx
import { ClerkProvider, Show, SignInButton, SignUpButton, UserButton } from '@clerk/nextjs';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ClerkProvider afterSignOutUrl="/">
          <header>
            <Show when="signed-out"><SignInButton /><SignUpButton /></Show>
            <Show when="signed-in"><UserButton /></Show>
          </header>
          {children}
        </ClerkProvider>
      </body>
    </html>
  );
}
```

### 2. Add clerkMiddleware, then protect at the resource

On Next.js 16 the file is `proxy.ts`; on Next.js 15 it is `middleware.ts`. The code is identical. `clerkMiddleware()` only attaches the session to the request and protects nothing by default.

```typescript
// proxy.ts (middleware.ts on Next.js 15)
import { clerkMiddleware } from '@clerk/nextjs/server';

export default clerkMiddleware();

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

Clerk's docs now say middleware is not the best place to protect routes: put the check in the code that reads or changes the data (pages, layouts that prefetch data, route handlers, server actions). `createRouteMatcher()` with `auth.protect()` inside `clerkMiddleware` still works but is deprecated, so write new code with per-resource checks.

```typescript
// app/dashboard/page.tsx
import { auth } from '@clerk/nextjs/server';

export default async function Dashboard() {
  await auth.protect(); // signed out: redirect to sign-in
  return <h1>Dashboard</h1>;
}
```

`auth.protect()` on a page redirects signed-out users to sign-in. In a route handler it returns 404 for an unauthenticated request (401 inside a server action), and a signed-in user lacking the required role or permission gets 404. If you want control over the response, call `auth()` and check `isAuthenticated` yourself (next step).

### 3. Read the session on the server

```typescript
import { auth, currentUser } from '@clerk/nextjs/server';

export default async function Page() {
  const { isAuthenticated, userId, orgId, orgRole, redirectToSignIn } = await auth();
  if (!isAuthenticated) return redirectToSignIn();

  const user = await currentUser(); // full user object (counts as a Backend API request)
  return <p>Hello {user?.firstName}</p>;
}
```

`auth()` is async in current versions: always `await` it. In a route handler return `NextResponse.json({ error: 'Unauthorized' }, { status: 401 })` when `userId` is null.

### 4. Client hooks and components

```tsx
'use client';
import { useAuth, useUser, useOrganization } from '@clerk/nextjs';

export function ProfileCard() {
  const { isSignedIn } = useAuth();
  const { user } = useUser();
  const { organization, membership } = useOrganization();
  if (!isSignedIn) return <p>Not signed in</p>;
  return <p>{user?.fullName} / {organization?.name} / {membership?.role}</p>;
}
```

Pre-built components from `@clerk/nextjs`: `SignIn`, `SignUp`, `UserButton`, `UserProfile`, `OrganizationSwitcher`, `OrganizationList`, `OrganizationProfile`. Mount the sign-in page at a catch-all route:

```tsx
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';
export default function SignInPage() { return <SignIn />; }
```

Show or hide UI with `<Show when="signed-in">`, `<Show when="signed-out">` or `<Show when={{ permission: 'org:invoices:create' }} fallback={...}>`; it replaces `SignedIn`, `SignedOut` and `Protect`. It only hides content visually, so never rely on it for security. The sign-out redirect is not a `UserButton` prop any more; pass `afterSignOutUrl="/"` to `<ClerkProvider>`. The old `afterSignInUrl`/`afterSignUpUrl` props became `fallbackRedirectUrl` and `signUpFallbackRedirectUrl`.

### 5. Organizations and roles

Enable Organizations in the dashboard first. The default roles are `org:admin` and `org:member`; extra roles and permissions (for example `org:projects:manage`) are defined under Organizations, Roles and permissions. There is no built-in `org:owner` role, so do not assume one exists.

```typescript
import { auth, clerkClient } from '@clerk/nextjs/server';

export async function createOrg(name: string) {
  const { userId } = await auth();
  const client = await clerkClient(); // async in current versions
  return client.organizations.createOrganization({ name, createdBy: userId! });
}

export async function inviteMember(organizationId: string, emailAddress: string) {
  const { userId } = await auth();
  const client = await clerkClient();
  return client.organizations.createOrganizationInvitation({
    organizationId,
    emailAddress,
    role: 'org:member',
    inviterUserId: userId!,
  });
}
```

Authorization checks with `has()`:

```typescript
const { has } = await auth();
if (!has({ role: 'org:admin' })) throw new Error('Forbidden');
if (!has({ permission: 'org:projects:manage' })) throw new Error('Forbidden');
```

Prefer permission checks: they survive role renames. On the server `has({ permission })` only evaluates custom permissions you defined in the dashboard (check the role for system permissions), and role or permission checks need an active organization; without one they return false.

### 6. Webhooks

Create an endpoint in the dashboard (Configure, Webhooks), subscribe to events, and copy the signing secret into `CLERK_WEBHOOK_SIGNING_SECRET`. The route must be reachable without a session (do not call `auth.protect()` there). `verifyWebhook` (from `@clerk/nextjs/webhooks`) reads that variable and validates the signature, so you no longer need to wire up `svix` yourself.

```typescript
// app/api/webhooks/clerk/route.ts
import { verifyWebhook } from '@clerk/nextjs/webhooks';
import type { NextRequest } from 'next/server';

export async function POST(req: NextRequest) {
  try {
    const evt = await verifyWebhook(req);
    switch (evt.type) {
      case 'user.created':
        await db.users.create({ data: {
          clerkId: evt.data.id,
          email: evt.data.email_addresses[0]?.email_address,
          name: `${evt.data.first_name ?? ''} ${evt.data.last_name ?? ''}`.trim(),
        }});
        break;
      case 'user.deleted':
        await db.users.deleteMany({ where: { clerkId: evt.data.id } });
        break;
      case 'organization.created':
        await db.orgs.create({ data: { clerkOrgId: evt.data.id, name: evt.data.name, slug: evt.data.slug } });
        break;
    }
    return new Response('OK', { status: 200 });
  } catch {
    return new Response('Invalid webhook', { status: 400 });
  }
}
```

Common events: `user.created`, `user.updated`, `user.deleted`, `organization.created`, `organization.updated`, `organizationMembership.created`, `organizationMembership.deleted`. Any non-2xx response makes Clerk retry; locally, expose the route with a tunnel.

### 7. JWT templates for external APIs

Create a template in the dashboard (Configure, JWT templates), for example `api-token` with claims `{ "userId": "{{user.id}}", "orgId": "{{org.id}}", "role": "{{org.role}}" }`.

```typescript
// client
const { getToken } = useAuth();
const token = await getToken({ template: 'api-token' });

// external service
import { verifyToken } from '@clerk/backend';

export async function verifyRequest(req: Request) {
  const token = req.headers.get('Authorization')?.replace('Bearer ', '');
  if (!token) throw new Error('Missing token');
  return verifyToken(token, { secretKey: process.env.CLERK_SECRET_KEY });
}
```

A JWT template token is not bound to a session (no `sid`). If you only need extra claims on the normal session token, use a customised session token instead.

### 8. Express

The old `@clerk/clerk-sdk-node` package has been superseded by `@clerk/express`, and `requireAuth()` is deprecated in favour of `clerkMiddleware()` plus `getAuth()`.

```bash
npm install @clerk/express
```

```typescript
import express from 'express';
import { clerkMiddleware, getAuth } from '@clerk/express';

const app = express();
app.use(clerkMiddleware());

app.get('/api/me', (req, res) => {
  const { isAuthenticated, userId, orgId } = getAuth(req);
  if (!isAuthenticated) return res.status(401).json({ error: 'Unauthorized' });
  res.json({ userId, orgId });
});
```

`@clerk/express` reads `CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` from the environment (no `NEXT_PUBLIC_` prefix outside Next.js).

## Examples

### Example 1: "Make everything under /dashboard private in my Next.js 15 app"

Run `npx -y clerk@latest init` (or add the two keys to `.env.local` and wrap `app/layout.tsx` in `<ClerkProvider>` inside `<body>`), keep the `proxy.ts`/`middleware.ts` from step 2, and protect the layout that fetches dashboard data:

```typescript
// app/dashboard/layout.tsx
import { auth } from '@clerk/nextjs/server';

export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
  await auth.protect();
  return <section>{children}</section>;
}
```

Check again in each route handler and server action under `/dashboard`: a layout alone does not re-run on client-side navigation.

Result: visiting `/dashboard` signed out redirects to `/sign-in`; after signing in the user lands back on `/dashboard`, and `await auth()` in the page returns their `userId`.

### Example 2: "Only org admins can create projects, and mirror users into Postgres"

Server action:

```typescript
'use server';
import { auth } from '@clerk/nextjs/server';

export async function createProject(name: string) {
  const { userId, orgId, has } = await auth();
  if (!orgId || !has({ role: 'org:admin' })) throw new Error('Forbidden');
  return db.projects.create({ data: { name, orgId, createdBy: userId } });
}
```

Add the `/api/webhooks/clerk` route from step 6, set `CLERK_WEBHOOK_SIGNING_SECRET=whsec_...`, and subscribe to `user.created` and `user.deleted`. Result: a member calling `createProject` gets "Forbidden", an admin gets the new row, and every sign-up appears in your `users` table within seconds.

## Guidelines

- Middleware is a convenience layer, not the only barrier: check `auth()` again in server components, route handlers and server actions.
- Verify every webhook signature (`verifyWebhook`); an unverified endpoint lets anyone forge user events.
- Webhooks are eventually consistent and can arrive out of order or twice: make handlers idempotent (upsert by `clerkId`). Keep a local copy of users and orgs rather than calling Clerk's Backend API on every request (rate limited).
- Never commit `CLERK_SECRET_KEY` or the webhook signing secret; only the publishable key is public.
- If your app uses Next.js 16, rename `middleware.ts` to `proxy.ts`; otherwise `clerkMiddleware` never runs and `auth()` fails to find it. Clerk Core 3 does not support Next.js 13 or 14.
- `hidePersonal` on `<OrganizationSwitcher>` removes personal workspaces for team-only products.
- Not a fit if you need a fully self-hosted identity provider; look at Keycloak or Auth.js instead.

Files in this skill

  • SKILL.md8.5 KB
  • _scores.json1.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…