.claude/skills/clerk/SKILL.md
Clerk authentication integration for Astro/Next.js. Use when implementing authentication, handling Clerk middleware, testing with Playwright, or debugging auth issues. Trigger phrases include "Clerk auth", "sign in", "authentication", "middleware", "E2E testing with Clerk".
npx skillsauth add Pratikkadam254/LinkedinOutreach clerkInstall 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.
Comprehensive guide for implementing and testing Clerk authentication, with special focus on Astro SSR integration and Playwright E2E testing.
┌─────────────────────────────────────────────────────────────┐
│ Browser (Client) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Clerk Frontend SDK (@clerk/astro) │ │
│ │ - Manages client-side session state │ │
│ │ - Provides <SignIn>, <UserButton> components │ │
│ │ - Sets localStorage tokens │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Server (Astro SSR) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ clerkMiddleware (@clerk/astro/server) │ │
│ │ - Validates HTTPOnly session cookies │ │
│ │ - Runs BEFORE any custom middleware logic │ │
│ │ - Sets Astro.locals.auth() │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Critical Understanding: Clerk's middleware validates sessions at the wrapper level BEFORE your callback executes. You cannot bypass authentication inside the middleware callback.
| Session Type | Created By | Server Validated | Use Case |
|--------------|-----------|------------------|----------|
| HTTPOnly Cookie | UI sign-in flow | ✅ Yes | Production, E2E tests |
| Client-side | @clerk/testing signIn() | ❌ No | Unit tests only |
| Backend API | sessions.create() | ⚠️ Partial | Limited use |
@clerk/testing's programmatic clerk.signIn() creates client-side sessions only. These are NOT recognized by Clerk's server-side middleware in Astro/Next.js SSR applications.
// ❌ This creates client-side session only - won't pass middleware
await clerk.signIn({
page,
signInParams: { strategy: 'password', identifier: email, password }
});
// User appears logged in (UserButton shows), but server redirects to /sign-in
Use actual UI sign-in flow with Clerk's +clerk_test email feature:
// ✅ This creates real server-validated session
// 1. Navigate to sign-in with testing token (bypasses bot detection)
await page.goto(`/sign-in?__clerk_testing_token=${testingToken}`);
// 2. Fill in email (MUST contain +clerk_test)
await page.fill('input[name="identifier"]', '[email protected]');
await page.click('button:has-text("Continue")');
// 3. Fill in password
await page.fill('input[type="password"]', password);
await page.click('button:has-text("Continue")');
// 4. Handle device verification with magic code
// See: clerk.com/docs/guides/development/testing/test-emails-and-phones
await enterVerificationCode(page, CLERK_TEST_VERIFICATION_CODE);
⚠️ CRITICAL: Test user emails MUST contain
+clerk_testfor automated testing to work. Without this suffix, Clerk requires real email verification which breaks CI/CD pipelines.
Any email with +clerk_test suffix is treated specially by Clerk:
Valid test email formats:
Invalid for automated testing:
[email protected] ❌ (no clerk_test in address)[email protected] ❌ (must use + plus-addressing)[email protected] ❌ (must use +clerk_test suffix format)Get the verification code: See Clerk's Test Emails Documentation for the magic verification code that works with +clerk_test emails.
💡 CI/CD Tip: Store test user emails in environment variables/secrets. Ensure all contain
+clerk_test:[email protected] [email protected]
Get a testing token to bypass bot detection:
import { createClerkClient } from '@clerk/backend';
const clerkClient = createClerkClient({
secretKey: process.env.CLERK_SECRET_KEY,
});
const token = await clerkClient.testingTokens.createTestingToken();
// Use as: /sign-in?__clerk_testing_token=${token.token}
// tests/e2e/global-setup.ts
import { createClerkClient } from '@clerk/backend';
const clerkClient = createClerkClient({
secretKey: process.env.CLERK_SECRET_KEY,
});
async function authenticateUser(page, email, password, storagePath) {
// 1. Get testing token
const { token } = await clerkClient.testingTokens.createTestingToken();
// 2. Navigate with token
await page.goto(`/sign-in?__clerk_testing_token=${token}`);
// 3. Fill email (must have +clerk_test)
await page.fill('input[name="identifier"]', email);
await page.click('button:has-text("Continue")');
// 4. Fill password
await page.fill('input[type="password"]', password);
await page.click('button:has-text("Continue")');
// 5. Handle device verification (code from Clerk docs)
// See: clerk.com/docs/guides/development/testing/test-emails-and-phones
await page.waitForTimeout(2000);
if (page.url().includes('factor-two')) {
const code = process.env.CLERK_TEST_CODE; // From Clerk docs
const inputs = page.locator('input[inputmode="numeric"]');
for (let i = 0; i < 6; i++) {
await inputs.nth(i).fill(code[i]);
}
}
// 6. Wait for redirect and save session
await page.waitForURL(url => !url.includes('/sign-in'));
await page.context().storageState({ path: storagePath });
}
// src/middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/astro/server";
const isPublicRoute = createRouteMatcher([
"/",
"/sign-in(.*)",
"/sign-up(.*)",
"/api/webhooks/(.*)",
]);
export const onRequest = clerkMiddleware((auth, context) => {
const { userId } = auth();
if (isPublicRoute(context.request)) {
return; // Allow public routes
}
if (!userId) {
return auth().redirectToSignIn();
}
});
// Check role inside middleware callback
export const onRequest = clerkMiddleware(async (auth, context) => {
const { userId } = auth();
if (!userId) {
return auth().redirectToSignIn();
}
// Check admin routes
if (context.request.url.includes('/admin')) {
const member = await memberQueries.findByClerkId(userId);
if (member?.role !== 'admin') {
return context.redirect('/unauthorized');
}
}
});
// src/pages/dashboard.astro
---
const auth = Astro.locals.auth();
const { userId, sessionClaims } = auth;
if (!userId) {
return Astro.redirect('/sign-in');
}
// Get user data from your database
const member = await memberQueries.findByClerkId(userId);
---
// For pre-rendered pages that need client-side auth
<script>
function checkAuth() {
if (window.Clerk?.loaded && !window.Clerk.user) {
window.Clerk.redirectToSignIn({ redirectUrl: window.location.href });
}
}
// Poll until Clerk loads
const interval = setInterval(() => {
if (window.Clerk?.loaded) {
clearInterval(interval);
checkAuth();
}
}, 100);
</script>
// src/pages/api/webhooks/clerk.ts
import { Webhook } from 'svix';
export const POST: APIRoute = async ({ request }) => {
const payload = await request.text();
const headers = Object.fromEntries(request.headers);
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET);
const event = wh.verify(payload, headers);
switch (event.type) {
case 'user.created':
// Create member record
break;
case 'user.updated':
// Sync user data
break;
}
return new Response('OK', { status: 200 });
};
Cause: Using @clerk/testing programmatic sign-in which only creates client-side sessions.
Fix: Use UI-based sign-in flow with testing tokens:
await page.goto(`/sign-in?__clerk_testing_token=${token}`);
// Then fill in the actual form
Cause: Clerk's bot protection blocking automated requests.
Fix: Include testing token in URL:
const token = await clerkClient.testingTokens.createTestingToken();
await page.goto(`/sign-in?__clerk_testing_token=${token.token}`);
Cause: Clerk requires email verification from new devices.
Fix: Use +clerk_test email suffix with Clerk's magic test code:
// Email MUST contain +clerk_test for magic code to work
const email = '[email protected]';
// Get the code from: clerk.com/docs/guides/development/testing/test-emails-and-phones
Common mistake: Using emails like [email protected] without clerk_test - the magic code won't work!
Cause: Page is pre-rendered (SSG) so server-side redirect doesn't work.
Fix: Use client-side redirect:
// In page frontmatter
export const prerender = true; // or remove for SSR
// In client script
if (!window.Clerk?.user) {
window.Clerk?.redirectToSignIn();
}
Cause: Route might be pre-rendered or middleware configuration issue.
Fix: Ensure SSR mode for protected routes:
// astro.config.mjs
export default defineConfig({
output: 'server', // or 'hybrid'
});
# .github/workflows/e2e-auth.yml
- name: Run authenticated E2E tests
env:
CLERK_SECRET_KEY: ${{ secrets.TEST_CLERK_SECRET_KEY }}
# CRITICAL: Emails MUST contain +clerk_test
TEST_ADMIN_EMAIL: ${{ secrets.TEST_ADMIN_EMAIL }}
TEST_ADMIN_PASSWORD: ${{ secrets.TEST_ADMIN_PASSWORD }}
run: npx playwright test
+clerk_test emails+clerk_test substringIf your local .env works but CI fails, sync your secrets:
# Script to update GitHub Secrets from .env
source .env
gh secret set TEST_ADMIN_EMAIL --body "$TEST_ADMIN_EMAIL"
gh secret set TEST_ADMIN_PASSWORD --body "$TEST_ADMIN_PASSWORD"
# Repeat for other test users...
Symptom: Local tests pass, CI tests fail at device verification step.
Root Cause: GitHub Secrets have emails WITHOUT +clerk_test:
# ❌ Wrong - magic code won't work
[email protected]
# ✅ Correct - magic code will work
[email protected]
Fix: Update GitHub Secrets with correctly formatted emails.
# Required for Clerk
PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxx
CLERK_WEBHOOK_SECRET=whsec_xxx
# For E2E testing - MUST contain +clerk_test
[email protected]
TEST_ADMIN_PASSWORD=xxx
[email protected]
TEST_MEMBER_PASSWORD=xxx
| Package | Purpose |
|---------|---------|
| @clerk/astro | Astro integration (components, middleware) |
| @clerk/backend | Server-side operations (testing tokens, user management) |
| @clerk/testing | Test utilities (limited - client-side only) |
| svix | Webhook signature verification |
Last updated: December 22, 2025 Added: CI/CD integration patterns and +clerk_test email requirements
development
Fast MVP shipping patterns for indie hackers - ruthless prioritization, lean development
development
Umbrella skill for all Convex development patterns. Routes to specific skills like convex-functions, convex-realtime, convex-agents, etc.
testing
Quick security audit checklist covering authentication, function exposure, argument validation, row-level access control, and environment variable handling
testing
Deep security review patterns for authorization logic, data access boundaries, action isolation, rate limiting, and protecting sensitive operations