Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Configure Clerk for multiple environments (dev, staging, production). Use when setting up environment-specific configurations, managing multiple Clerk instances, or implementing environment promotion. Trigger with phrases like "clerk environments", "clerk staging", "clerk dev prod", "clerk multi-environment".
.claude/skills/jeremylongshore-clerk-multi-env-setup/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 89% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 7% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 170% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 97% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 139% | 0% |
Configure Clerk across development, staging, and production environments with separate instances, environment-aware configuration, and safe promotion workflows.
Create separate Clerk instances in the Dashboard for each environment:
| Environment | Instance | Key Prefix | Domain | |-------------|----------|------------|--------| | Development | my-app-dev | pk_test_ / sk_test_ | localhost:3000 | | Staging | my-app-staging | pk_test_ / sk_test_ | staging.myapp.com | | Production | my-app-prod | pk_live_ / sk_live_ | myapp.com |
bash# .env.local (development - git-ignored) NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_dev... CLERK_SECRET_KEY=sk_test_dev... CLERK_WEBHOOK_SECRET=whsec_dev... # .env.staging (staging) NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_staging... CLERK_SECRET_KEY=sk_test_staging... CLERK_WEBHOOK_SECRET=whsec_staging... # .env.production (production) NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_prod... CLERK_SECRET_KEY=sk_live_prod... CLERK_WEBHOOK_SECRET=whsec_prod...
typescript// lib/clerk-config.ts type ClerkEnv = 'development' | 'staging' | 'production' function getClerkEnv(): ClerkEnv { const key = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || '' if (key.startsWith('pk_live_')) return 'production' if (process.env.VERCEL_ENV === 'preview') return 'staging' return 'development' } export const clerkConfig = { env: getClerkEnv(), get isDev() { return this.env === 'development' }, get isProd() { return this.env === 'production' }, signInUrl: '/sign-in', signUpUrl: '/sign-up', afterSignInUrl: '/dashboard', get allowedRedirectOrigins() { switch (this.env) { case 'production': return ['https://myapp.com'] case 'staging': return ['https://staging.myapp.com'] default: return ['http://localhost:3000'] } }, }
typescript// lib/validate-env.ts export function validateClerkEnv() { const required = [ 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY', 'CLERK_SECRET_KEY', ] const missing = required.filter((key) => !process.env[key]) if (missing.length > 0) { throw new Error(`Missing Clerk env vars: ${missing.join(', ')}`) } const pk = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY! const sk = process.env.CLERK_SECRET_KEY! // Prevent key mismatch (test keys with live keys) const pkIsLive = pk.startsWith('pk_live_') const skIsLive = sk.startsWith('sk_live_') if (pkIsLive !== skIsLive) { throw new Error('Clerk key mismatch: publishable and secret keys must be from same environment') } // Warn if using live keys in development if (pkIsLive && process.env.NODE_ENV === 'development') { console.warn('WARNING: Using production Clerk keys in development mode') } }
Call at app startup:
typescript// app/layout.tsx import { validateClerkEnv } from '@/lib/validate-env' validateClerkEnv()
typescript// app/api/webhooks/clerk/route.ts import { headers } from 'next/headers' import { Webhook } from 'svix' export async function POST(req: Request) { // Each environment uses its own webhook secret const secret = process.env.CLERK_WEBHOOK_SECRET if (!secret) { console.error('CLERK_WEBHOOK_SECRET not configured for this environment') return new Response('Server error', { status: 500 }) } // Verification logic (same across environments) const headerPayload = await headers() const wh = new Webhook(secret) const body = await req.text() try { const evt = wh.verify(body, { 'svix-id': headerPayload.get('svix-id')!, 'svix-timestamp': headerPayload.get('svix-timestamp')!, 'svix-signature': headerPayload.get('svix-signature')!, }) // Handle event... return new Response('OK', { status: 200 }) } catch { return new Response('Invalid signature', { status: 400 }) } }
yaml# .github/workflows/deploy.yml name: Deploy on: push: branches: [main, staging] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set environment run: | if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then echo "DEPLOY_ENV=production" >> $GITHUB_ENV else echo "DEPLOY_ENV=staging" >> $GITHUB_ENV fi - name: Deploy to Vercel env: NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets[format('CLERK_PK_{0}', env.DEPLOY_ENV)] }} CLERK_SECRET_KEY: ${{ secrets[format('CLERK_SK_{0}', env.DEPLOY_ENV)] }} run: vercel deploy --prod
| Error | Cause | Solution | |-------|-------|----------| | Key mismatch error | pk_test_ with sk_live_ | Ensure both keys from same Clerk instance | | Webhook signature fails | Wrong secret for environment | Verify CLERK_WEBHOOK_SECRET matches instance | | User not found | Querying wrong environment | Check you are hitting correct Clerk instance | | OAuth redirect fails | Domain not configured | Add environment domain in Clerk Dashboard |
bash# Set env vars for Vercel preview deployments vercel env add NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY preview vercel env add CLERK_SECRET_KEY preview
Proceed to clerk-observability for monitoring and logging.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 14,988 | 18,617 | +24% | 1 | 1 | 0% | 3,238 | 6,107 | +89% | 0 | 0 | — |
case-02 | fail→fail | 15,004 | 12,136 | -19% | 1 | 1 | 0% | 3,180 | 4,560 | +43% | 0 | 0 | — |
case-03 | fail→pass | 17,580 | 10,139 | -42% | 1 | 1 | 0% | 3,824 | 4,074 | +7% | 0 | 0 | — |
case-04 | pass→pass | 14,023 | 16,223 | +16% | 1 | 1 | 0% | 2,685 | 5,181 | +93% | 0 | 0 | — |
case-05 | pass→pass | 18,658 | 17,709 | -5% | 1 | 1 | 0% | 3,313 | 5,062 | +53% | 0 | 0 | — |
case-06 | pass→pass | 14,537 | 15,579 | +7% | 1 | 1 | 0% | 2,544 | 4,535 | +78% | 0 | 0 | — |
case-07 | pass→pass | 5,888 | 3,742 | -36% | 1 | 1 | 0% | 1,031 | 2,453 | +138% | 0 | 0 | — |
case-08 | pass→pass | 8,906 | 4,175 | -53% | 1 | 1 | 0% | 1,555 | 2,289 | +47% | 0 | 0 | — |
case-09 | fail→fail | 10,944 | 10,932 | -0% | 1 | 1 | 0% | 1,914 | 3,679 | +92% | 0 | 0 | — |
case-10 | pass→pass | 13,678 | 9,350 | -32% | 1 | 1 | 0% | 2,510 | 3,672 | +46% | 0 | 0 | — |
case-11 | fail→pass | 41,335 | 8,158 | -80% | 1 | 1 | 0% | 1,250 | 3,373 | +170% | 0 | 0 | — |
case-12 | fail→pass | 7,555 | 4,554 | -40% | 1 | 1 | 0% | 1,242 | 2,451 | +97% | 0 | 0 | — |
case-13 | pass→pass | 14,552 | 12,425 | -15% | 1 | 1 | 0% | 2,714 | 4,187 | +54% | 0 | 0 | — |
case-14 | pass→pass | 8,395 | 3,876 | -54% | 1 | 1 | 0% | 1,463 | 2,529 | +73% | 0 | 0 | — |
case-15 | pass→pass | 9,546 | 4,721 | -51% | 1 | 1 | 0% | 1,568 | 2,589 | +65% | 0 | 0 | — |
case-16 | pass→pass | 12,745 | 3,780 | -70% | 1 | 1 | 0% | 2,218 | 2,507 | +13% | 0 | 0 | — |
case-17 | pass→fail | 13,785 | 12,348 | -10% | 1 | 1 | 0% | 2,528 | 3,035 | +20% | 0 | 0 | — |
case-18 | fail→pass | 5,154 | 1,839 | -64% | 1 | 1 | 0% | 868 | 2,077 | +139% | 0 | 0 | — |
case-19 | pass→pass | 7,318 | 2,901 | -60% | 1 | 1 | 0% | 1,342 | 2,269 | +69% | 0 | 0 | — |
case-20 | fail→pass | 12,093 | 5,140 | -57% | 1 | 1 | 0% | 2,107 | 2,792 | +33% | 0 | 0 | — |
case-21 | pass→pass | 8,364 | 1,587 | -81% | 1 | 1 | 0% | 1,248 | 2,095 | +68% | 0 | 0 | — |
case-22 | pass→pass | 13,286 | 10,392 | -22% | 1 | 1 | 0% | 2,204 | 3,656 | +66% | 0 | 0 | — |
case-23 | fail→pass | 10,476 | 4,655 | -56% | 1 | 1 | 0% | 1,623 | 2,601 | +60% | 0 | 0 | — |
case-24 | fail→pass | 10,488 | 2,521 | -76% | 1 | 1 | 0% | 1,578 | 2,184 | +38% | 0 | 0 | — |
case-25 | fail→pass | 8,749 | 3,403 | -61% | 1 | 1 | 0% | 1,572 | 2,338 | +49% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted, and 24 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +32 percentage points is the difference between those two pass rates over the 24 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.