Clerk Auth skill for Codex

Clerk sign-in with checks where the data lives. Codex adds Clerk the current way: clerkMiddleware in proxy.ts or middleware.ts, ClerkProvider inside body, the Core 3 Show component, and auth() awaited in every page, route handler and server action that reads data. Authorisation uses org permissions or backend-written metadata, users sync to your database through verified, deduplicated webhooks, and the move to a production instance follows a checklist.

  • Integrations
  • $clerk-auth
  • 211 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- clerk-auth

Installs to ~/.agents/skills/clerk-auth/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $clerk-auth, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Integrates Clerk authentication into Next.js App Router, React and Node backends - ClerkProvider, clerkMiddleware in proxy.ts or middleware.ts, server-side auth() and auth.protect(), the Core 3 Show component, organizations with roles and permissions, user metadata, syncing users to your database with verified webhooks, verifying Clerk sessions in separate APIs, and moving from development to production instances. Use when adding Clerk sign-in to an app, protecting pages, route handlers or server actions, upgrading Clerk SDKs, or debugging redirect loops and signed-out users on the server.

Use it when

  • You are adding Clerk to a Next.js, React or Express app
  • Pages, route handlers or server actions need auth or org permission checks
  • A local users table must stay in sync with Clerk
  • You are upgrading Clerk SDKs or moving from a development to a production instance

Use something else when

  • You are choosing between auth providers or self-hosted libraries: use auth-libraries
  • The project uses Firebase Authentication: use firebase-auth

How to check the result

Signed out, protected routes redirect or return 401; signed in without the permission, APIs return 403; replaying a webhook with the same svix-id creates no duplicate.

The mistake it prevents

Protecting pages only in middleware or the UI while server actions and route handlers skip their own auth() check.

~/.agents/skills/clerk-auth/SKILL.md

--- name: clerk-auth description: Integrates Clerk authentication into Next.js App Router, React and Node backends - ClerkProvider, clerkMiddleware in proxy.ts or middleware.ts,… license: MIT ---

Done well means: every protected resource checks auth on the server where the data is read (not only in middleware or the UI), authorisation uses roles, permissions or server-written metadata (never client-writable data), the local user table stays in sync through verified, idempotent webhooks, and production uses its own instance, domain and OAuth credentials.

Inspect first

- [ ] Read AGENTS.md and package.json: @clerk/nextjs version, Next.js version, router type
- [ ] Next.js 16+: the file is proxy.ts; Next.js 15: middleware.ts (same code)
- [ ] Clerk Core 3 (2026) needs Next.js 15.2.3+ and Node 20.9+; it replaced SignedIn/SignedOut/Protect with <Show>
- [ ] Find where auth is checked today: middleware only, layouts, route handlers, server actions
- [ ] Check env: NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY (never in client code)
- [ ] Is there a local users table? Then webhooks are needed (CLERK_WEBHOOK_SIGNING_SECRET)

For upgrades, run npx @clerk/upgrade first (needs network); it applies most codemods, then fix what it reports. Package installs need network approval in a Codex sandbox.

Next.js App Router setup

// proxy.ts (Next.js 16+) or middleware.ts (Next.js 15)
import { clerkMiddleware } from "@clerk/nextjs/server";

export default clerkMiddleware();

export const config = {
  matcher: [
    // Skip Next.js internals and static files
    "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
    "/(api|trpc)(.*)",
  ],
};

Copy the current matcher from Clerk’s docs or the generated file when scaffolding; it also covers Clerk’s own frontend API routes. The middleware makes auth state available; it is not where protection should live.

// app/layout.tsx  (Core 3: ClerkProvider inside <body>)
import { ClerkProvider, Show, SignInButton, UserButton } from "@clerk/nextjs";

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

On Core 2 (@clerk/nextjs v6) the equivalents are <SignedIn>/<SignedOut>/<Protect>; do not mix both styles in one codebase.

Protect on the server, where data is read

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

export default async function Dashboard() {
  const { userId } = await auth.protect();          // signed-out users are sent to sign-in
  const projects = await db.project.findMany({ where: { ownerId: userId } });
  return <ProjectList projects={projects} />;
}
// app/api/invoices/route.ts
import { auth } from "@clerk/nextjs/server";

export async function POST(req: Request) {
  const { userId, orgId, has } = await auth();
  if (!userId) return new Response("Unauthorized", { status: 401 });
  if (!orgId || !has({ permission: "org:invoices:create" })) return new Response("Forbidden", { status: 403 });
  // scope every query by orgId, never by an ID taken from the request body alone
}
CheckUseNotes
Signed inawait auth() -> userIdauth() is async in current SDKs; always await
Signed in or redirectawait auth.protect()Route handlers/actions get 401 instead of a redirect
Rolehas({ role: "org:admin" })Organization roles
Permissionhas({ permission: "org:billing:manage" })Prefer permissions over role names
Plan / feature (Clerk Billing)has({ plan: "pro" }), has({ feature: "exports" })Only if the app uses Clerk Billing
Full user objectawait currentUser()Costs a Backend API call; avoid on hot paths

Route matching helpers in middleware (createRouteMatcher) are deprecated in Core 3; Clerk recommends resource-level checks like the ones above. Server actions are public HTTP endpoints: each one needs its own auth() check.

Metadata and custom claims

MetadataWho writesUse for
publicMetadataBackend onlyRoles or flags the UI may read (e.g. { plan: "pro" })
privateMetadataBackend onlyInternal IDs, never sent to the browser
unsafeMetadataBrowser can writeUser preferences; never for authorisation
import { clerkClient } from "@clerk/nextjs/server";

const client = await clerkClient();
await client.users.updateUserMetadata(userId, { publicMetadata: { onboarded: true } });

To read metadata without an API call, add it to the session token (Dashboard -> Sessions -> customize session token, e.g. {"metadata": "{{user.public_metadata}}"}) and read sessionClaims.metadata. Keep custom claims small: the session token travels in a cookie with a size budget of roughly 1.2 KB for custom claims.

Sync users with webhooks

// app/api/webhooks/clerk/route.ts
import { verifyWebhook } from "@clerk/nextjs/webhooks";

export async function POST(req: Request) {
  let evt;
  try {
    evt = await verifyWebhook(req);                 // uses CLERK_WEBHOOK_SIGNING_SECRET
  } catch {
    return new Response("Invalid signature", { status: 400 });
  }
  const eventId = req.headers.get("svix-id")!;
  if (await alreadyProcessed(eventId)) return new Response("ok");

  switch (evt.type) {
    case "user.created":
    case "user.updated":
      await db.user.upsert({
        where: { clerkId: evt.data.id },
        create: { clerkId: evt.data.id, email: evt.data.email_addresses[0]?.email_address },
        update: { email: evt.data.email_addresses[0]?.email_address },
      });
      break;
    case "user.deleted":
      await db.user.update({ where: { clerkId: evt.data.id! }, data: { deletedAt: new Date() } });
      break;
  }
  await markProcessed(eventId);
  return new Response("ok");
}
  • The webhook route must be reachable while signed out; do not protect it.
  • Deliveries can repeat and arrive out of order; upsert, dedupe on svix-id, and compare updated_at when it matters.
  • Do not block sign-up on the webhook: if a request arrives before the user row exists, create it lazily from auth() data.
  • Locally, expose the route with a tunnel and use the dashboard’s “Testing” tab to send events.

Other backends

  • Express: @clerk/express with clerkMiddleware() and getAuth(req); reject when userId is null.
  • Any other API receiving requests from a Clerk front end: send the session token as Authorization: Bearer <token from getToken()> and verify it with the backend SDK’s request authentication (authenticateRequest with authorizedParties set to your front-end origins). Session tokens live about 60 seconds; do not cache them server side.
  • JWT templates are for third-party services that need a token in their own format; you do not need one for your own API.

Development to production

- [ ] Create the production instance; set its own publishable and secret keys per environment
- [ ] Add DNS records for the production domain as Clerk lists them; wait for verification
- [ ] Configure your own OAuth client IDs/secrets for each social provider (dev uses shared ones)
- [ ] Recreate webhooks for the production endpoint with a new signing secret
- [ ] Set allowed redirect URLs / authorized parties to production origins only
- [ ] Confirm with the user before switching live traffic to the new keys

Gotchas

  • Redirect loops: usually sign-in page itself protected, or NEXT_PUBLIC_CLERK_SIGN_IN_URL pointing to a route that requires auth.
  • auth() returns userId: null in a route that middleware does not match; check the matcher.
  • With Next.js cacheComponents, components calling auth() or currentUser() must sit inside <Suspense>.
  • Users can belong to several organizations; orgId is the active one and may be null.
  • Deleting users through the Backend API is permanent; confirm with the user first.

Verification

  • Signed out: protected pages redirect, protected API routes return 401, the webhook route still accepts a signed test event.
  • Signed in without the permission: API returns 403; UI hides the action.
  • Webhook replay of the same svix-id produces no duplicate row.
  • @clerk/testing with Playwright can create testing tokens to run sign-in flows in CI without bot protection blocking them.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Clerk Auth"
  short_description: "Clerk sign-in with checks where the data lives"
  default_prompt: "$clerk-auth Add Clerk sign-in to this Next.js app and protect the dashboard and API routes"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem