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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- clerk-auth Installs to ~/.agents/skills/clerk-auth/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install clerk-auth The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project clerk-auth Installs to .agents/skills/clerk-auth/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/clerk-auth.zipunzip -o clerk-auth.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) clerk-auth PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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
}
| Check | Use | Notes |
|---|---|---|
| Signed in | await auth() -> userId | auth() is async in current SDKs; always await |
| Signed in or redirect | await auth.protect() | Route handlers/actions get 401 instead of a redirect |
| Role | has({ role: "org:admin" }) | Organization roles |
| Permission | has({ 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 object | await 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
| Metadata | Who writes | Use for |
|---|---|---|
publicMetadata | Backend only | Roles or flags the UI may read (e.g. { plan: "pro" }) |
privateMetadata | Backend only | Internal IDs, never sent to the browser |
unsafeMetadata | Browser can write | User 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 compareupdated_atwhen 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/expresswithclerkMiddleware()andgetAuth(req); reject whenuserIdis 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 (authenticateRequestwithauthorizedPartiesset 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_URLpointing to a route that requires auth. auth()returnsuserId: nullin a route that middleware does not match; check the matcher.- With Next.js
cacheComponents, components callingauth()orcurrentUser()must sit inside<Suspense>. - Users can belong to several organizations;
orgIdis 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-idproduces no duplicate row. @clerk/testingwith 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.yaml
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.