| name | clerk-multi-env-setup |
| description | 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".
|
| allowed-tools | Read, Write, Edit, Bash(npm:*), Grep |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Clerk Multi-Environment Setup
Overview
Configure Clerk across development, staging, and production environments.
Prerequisites
- Clerk account with multiple instances
- Understanding of environment management
- CI/CD pipeline configured
Instructions
Step 1: Create Clerk Instances
Create separate Clerk instances for each environment in the Clerk Dashboard:
myapp-dev - Development
myapp-staging - Staging
myapp-prod - Production
Step 2: Environment Configuration
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_dev_...
CLERK_SECRET_KEY=sk_test_dev_...
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_staging_...
CLERK_SECRET_KEY=sk_test_staging_...
NEXT_PUBLIC_APP_ENV=staging
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_SECRET_KEY=sk_live_...
NEXT_PUBLIC_APP_ENV=production
Step 3: Environment-Aware Configuration
type Environment = 'development' | 'staging' | 'production'
interface ClerkConfig {
signInUrl: string
signUpUrl: string
afterSignInUrl: string
afterSignUpUrl: string
debug: boolean
}
const configs: Record<Environment, ClerkConfig> = {
development: {
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
afterSignInUrl: '/dashboard',
afterSignUpUrl: '/onboarding',
debug: true
},
staging: {
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
afterSignInUrl: '/dashboard',
afterSignUpUrl: '/onboarding',
debug: true
},
production: {
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
afterSignInUrl: '/dashboard',
: ,
:
}
}
(): {
env = (process.. ) ||
configs[env]
}
() {
pk = process..
env = process..
(env === && pk?.()) {
()
}
(env !== && pk?.()) {
.()
}
}
Step 4: ClerkProvider Configuration
import { ClerkProvider } from '@clerk/nextjs'
import { getClerkConfig, validateClerkEnvironment } from '@/lib/clerk-config'
validateClerkEnvironment()
export default function RootLayout({ children }) {
const config = getClerkConfig()
return (
<ClerkProvider
signInUrl={config.signInUrl}
signUpUrl={config.signUpUrl}
afterSignInUrl={config.afterSignInUrl}
afterSignUpUrl={config.afterSignUpUrl}
>
<html>
<body>
{config.debug && <EnvironmentBanner />}
{children}
</body>
</html>
</ClerkProvider>
)
}
function EnvironmentBanner() {
const env = process.env.NEXT_PUBLIC_APP_ENV
if (env === 'production') return null
const colors = {
: ,
:
}
(
)
}
Step 5: Webhook Configuration Per Environment
import { headers } from 'next/headers'
const WEBHOOK_SECRETS = {
development: process.env.CLERK_WEBHOOK_SECRET_DEV,
staging: process.env.CLERK_WEBHOOK_SECRET_STAGING,
production: process.env.CLERK_WEBHOOK_SECRET
}
export async function POST(req: Request) {
const env = process.env.NEXT_PUBLIC_APP_ENV as keyof typeof WEBHOOK_SECRETS
const WEBHOOK_SECRET = WEBHOOK_SECRETS[env]
if (!WEBHOOK_SECRET) {
console.error(`No webhook secret for environment: ${env}`)
return Response.json({ error: 'Configuration error' }, { status: 500 })
}
}
Step 6: CI/CD Environment Promotion
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
echo "CLERK_PUBLISHABLE_KEY=${{ secrets.CLERK_PUBLISHABLE_KEY_PROD }}" >> $GITHUB_ENV
echo "CLERK_SECRET_KEY=${{ secrets.CLERK_SECRET_KEY_PROD }}" >> $GITHUB_ENV
else
echo "DEPLOY_ENV=staging" >> $GITHUB_ENV
echo "CLERK_PUBLISHABLE_KEY=${{ secrets.CLERK_PUBLISHABLE_KEY_STAGING }}" >> $GITHUB_ENV
echo "CLERK_SECRET_KEY=${{ secrets.CLERK_SECRET_KEY_STAGING }}" >> $GITHUB_ENV
fi
- name: Build
run: npm run build
env:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ env.CLERK_PUBLISHABLE_KEY }}
NEXT_PUBLIC_APP_ENV: ${{ env.DEPLOY_ENV }}
- name: Deploy to Vercel
run: vercel deploy
Step 7: User Data Isolation
export async function syncUser(clerkUser: any) {
const env = process.env.NEXT_PUBLIC_APP_ENV
await db.user.upsert({
where: {
clerkId_environment: {
clerkId: clerkUser.id,
environment: env
}
},
update: { },
create: {
clerkId: clerkUser.id,
environment: env,
}
})
}
Environment Matrix
| Environment | Keys | Domain | Data |
|---|
| Development | pk_test_dev | localhost:3000 | Dev DB |
| Staging | pk_test_staging | staging.myapp.com | Staging DB |
| Production | pk_live | myapp.com | Prod DB |
Output
- Separate Clerk instances per environment
- Environment-aware configuration
- Webhook handling per environment
- CI/CD pipeline configured
Best Practices
- Never share keys between environments
- Use test keys for non-production
- Validate key/environment match at startup
- Separate webhook secrets per environment
- Isolate user data by environment
Error Handling
| Error | Cause | Solution |
|---|
| Wrong environment keys | Misconfiguration | Validate at startup |
| Webhook signature fails | Wrong secret | Check env-specific secret |
| User not found | Env mismatch | Check environment isolation |
Resources
Next Steps
Proceed to clerk-observability for monitoring and logging.