Authentication integration patterns for Clerk with Astro, Playwright E2E testing, and CI/CD workflows.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
scripts/setup-test-users.tsAuthentication integration patterns for Clerk with Astro, Playwright E2E testing, and CI/CD workflows.
// Use window.Clerk directly for client-side auth checks
function checkAuth() {
if (window.Clerk?.loaded && !window.Clerk.user) {
window.Clerk.redirectToSignIn({ redirectUrl: window.location.href });
}
}
// Poll for Clerk to load
const interval = setInterval(() => {
if (window.Clerk?.loaded) {
clearInterval(interval);
checkAuth();
}
}, 100);
// API routes needing locals.auth MUST set prerender = false
export const prerender = false;
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ locals }) => {
const { userId } = locals.auth();
if (!userId) {
return new Response('Unauthorized', { status: 401 });
}
// ... handle authenticated request
};
| Variable | Purpose | Required |
|---|---|---|
PUBLIC_CLERK_PUBLISHABLE_KEY | Frontend Clerk initialization | Yes |
CLERK_SECRET_KEY | Backend API calls | Yes |
CLERK_WEBHOOK_SECRET | Webhook signature verification | For webhooks |
CLERK_PUBLISHABLE_KEY | @clerk/testing (no PUBLIC_ prefix) | For E2E tests |
npm install --save-dev @clerk/testing
import { clerkSetup, clerk } from '@clerk/testing/playwright';
import { chromium } from '@playwright/test';
export default async function globalSetup() {
// Initialize Clerk testing utilities
await clerkSetup();
const browser = await chromium.launch();
const page = await browser.newPage();
// Navigate first (Clerk needs a page context)
await page.goto('/');
// Programmatic sign-in (no UI interaction)
await clerk.signIn({
page,
signInParams: {
strategy: 'password',
identifier: process.env.TEST_USER_EMAIL,
password: process.env.TEST_USER_PASSWORD,
},
});
// Navigate to trigger session recognition
await page.goto('/');
// Save storage state for test reuse
await page.context().storageState({ path: 'tests/.auth/user.json' });
await browser.close();
}
Environment variable naming: @clerk/testing requires CLERK_PUBLISHABLE_KEY (not PUBLIC_CLERK_PUBLISHABLE_KEY)
Password auth required: Test users must have password authentication enabled in Clerk dashboard (OAuth-only users won't work)
Navigate after sign-in: clerk.signIn() sets up the session but doesn't navigate. Call page.goto() after to capture authenticated state.
Session storage: Save storageState to reuse sessions across tests for faster execution.
Use the Clerk Backend API to create or update test users with password auth:
import { createClerkClient } from '@clerk/backend';
const clerkClient = createClerkClient({
secretKey: process.env.CLERK_SECRET_KEY,
});
async function createOrUpdateUser(email: string, password: string) {
// Check if user exists
const users = await clerkClient.users.getUserList({
emailAddress: [email],
});
if (users.data[0]) {
// Update password for existing user
await clerkClient.users.updateUser(users.data[0].id, { password });
} else {
// Create new user with password
await clerkClient.users.createUser({
emailAddress: [email],
password,
skipPasswordChecks: true,
});
}
}
steps:
- name: Install dependencies
run: npm ci
- name: Setup test users in Clerk
env:
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
run: npm run test:setup-users
- name: Seed test database
run: npm run db:seed:test
- name: Build application
run: npm run build
- name: Run E2E tests
env:
CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_PUBLISHABLE_KEY }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
run: npm run test:e2e
| Event | Use Case |
|---|---|
user.created | Auto-create member record in database |
user.updated | Sync profile changes |
user.deleted | Remove member record |
session.created | Track login activity |
import { Webhook } from 'svix';
import type { WebhookEvent } from '@clerk/backend';
export const POST: APIRoute = async ({ request }) => {
const payload = await request.text();
const headers = {
'svix-id': request.headers.get('svix-id')!,
'svix-timestamp': request.headers.get('svix-timestamp')!,
'svix-signature': request.headers.get('svix-signature')!,
};
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
const event = wh.verify(payload, headers) as WebhookEvent;
switch (event.type) {
case 'user.created':
// Create member in database
break;
case 'user.updated':
// Update member profile
break;
case 'user.deleted':
// Remove member
break;
}
return new Response('OK', { status: 200 });
};
Use Clerk's testing tokens to bypass bot detection:
import { clerkSetup } from '@clerk/testing/playwright';
// clerkSetup() automatically handles testing tokens
await clerkSetup();
Symptom: After entering email/password, page redirects to GitHub/Google OAuth.
Cause: User account is OAuth-only, no password set.
Solution: Use scripts/setup-test-users.ts to provision users with password auth via Backend API.
Symptom: User appears logged out after navigation.
Cause: Storage state not saved or loaded correctly.
Solution:
await context.storageState({ path: 'auth.json' })browser.newContext({ storageState: 'auth.json' })Symptom: @clerk/testing throws "CLERK_PUBLISHABLE_KEY required"
Cause: Using PUBLIC_CLERK_PUBLISHABLE_KEY instead of CLERK_PUBLISHABLE_KEY
Solution: Set both in CI environment:
env:
PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_KEY }}
CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_KEY }}