clerk-multi-env-setup

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Clerk Multi-Environment Setup

Clerk 多环境配置

Overview

概述

Configure Clerk across development, staging, and production environments.
在开发、预发布和生产环境中配置Clerk。

Prerequisites

前置条件

  • Clerk account with multiple instances
  • Understanding of environment management
  • CI/CD pipeline configured
  • 拥有多实例的Clerk账号
  • 了解环境管理知识
  • 已配置CI/CD流水线

Instructions

操作步骤

Step 1: Create Clerk Instances

步骤1:创建Clerk实例

Create separate Clerk instances for each environment in the Clerk Dashboard:
  • myapp-dev
    - Development
  • myapp-staging
    - Staging
  • myapp-prod
    - Production
在Clerk控制台为每个环境创建独立的Clerk实例:
  • myapp-dev
    - 开发环境
  • myapp-staging
    - 预发布环境
  • myapp-prod
    - 生产环境

Step 2: Environment Configuration

步骤2:环境变量配置

bash
undefined
bash
undefined

.env.development.local

.env.development.local

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_dev_... CLERK_SECRET_KEY=sk_test_dev_... NEXT_PUBLIC_APP_ENV=development

.env.staging.local

.env.staging.local

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_test_staging_... CLERK_SECRET_KEY=sk_test_staging_... NEXT_PUBLIC_APP_ENV=staging

.env.production.local

.env.production.local

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_... CLERK_SECRET_KEY=sk_live_... NEXT_PUBLIC_APP_ENV=production
undefined
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_... CLERK_SECRET_KEY=sk_live_... NEXT_PUBLIC_APP_ENV=production
undefined

Step 3: Environment-Aware Configuration

步骤3:环境感知配置

typescript
// lib/clerk-config.ts
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',
    afterSignUpUrl: '/onboarding',
    debug: false
  }
}

export function getClerkConfig(): ClerkConfig {
  const env = (process.env.NEXT_PUBLIC_APP_ENV as Environment) || 'development'
  return configs[env]
}

// Validate environment at startup
export function validateClerkEnvironment() {
  const pk = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
  const env = process.env.NEXT_PUBLIC_APP_ENV

  if (env === 'production' && pk?.startsWith('pk_test_')) {
    throw new Error('Production environment using test keys!')
  }

  if (env !== 'production' && pk?.startsWith('pk_live_')) {
    console.warn('Non-production environment using live keys')
  }
}
typescript
// lib/clerk-config.ts
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',
    afterSignUpUrl: '/onboarding',
    debug: false
  }
}

export function getClerkConfig(): ClerkConfig {
  const env = (process.env.NEXT_PUBLIC_APP_ENV as Environment) || 'development'
  return configs[env]
}

// Validate environment at startup
export function validateClerkEnvironment() {
  const pk = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
  const env = process.env.NEXT_PUBLIC_APP_ENV

  if (env === 'production' && pk?.startsWith('pk_test_')) {
    throw new Error('Production environment using test keys!')
  }

  if (env !== 'production' && pk?.startsWith('pk_live_')) {
    console.warn('Non-production environment using live keys')
  }
}

Step 4: ClerkProvider Configuration

步骤4:ClerkProvider配置

typescript
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'
import { getClerkConfig, validateClerkEnvironment } from '@/lib/clerk-config'

// Validate on startup
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 = {
    development: 'bg-green-500',
    staging: 'bg-yellow-500'
  }

  return (
    <div className={`${colors[env]} text-white text-center text-sm py-1`}>
      {env?.toUpperCase()} ENVIRONMENT
    </div>
  )
}
typescript
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'
import { getClerkConfig, validateClerkEnvironment } from '@/lib/clerk-config'

// Validate on startup
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 = {
    development: 'bg-green-500',
    staging: 'bg-yellow-500'
  }

  return (
    <div className={`${colors[env]} text-white text-center text-sm py-1`}>
      {env?.toUpperCase()} ENVIRONMENT
    </div>
  )
}

Step 5: Webhook Configuration Per Environment

步骤5:按环境配置Webhook

typescript
// app/api/webhooks/clerk/route.ts
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 })
  }

  // ... rest of webhook handling
}
typescript
// app/api/webhooks/clerk/route.ts
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 })
  }

  // ... rest of webhook handling
}

Step 6: CI/CD Environment Promotion

步骤6:CI/CD环境升级配置

yaml
undefined
yaml
undefined

.github/workflows/deploy.yml

.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
        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 --prod
    env:
      VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
undefined
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 --prod
    env:
      VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
undefined

Step 7: User Data Isolation

步骤7:用户数据隔离

typescript
// lib/user-sync.ts
// Ensure user data doesn't leak between environments

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,
      // ... other fields
    }
  })
}
typescript
// lib/user-sync.ts
// Ensure user data doesn't leak between environments

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,
      // ... other fields
    }
  })
}

Environment Matrix

环境矩阵

EnvironmentKeysDomainData
Developmentpk_test_devlocalhost:3000Dev DB
Stagingpk_test_stagingstaging.myapp.comStaging DB
Productionpk_livemyapp.comProd DB
环境密钥域名数据
开发环境pk_test_devlocalhost:3000开发数据库
预发布环境pk_test_stagingstaging.myapp.com预发布数据库
生产环境pk_livemyapp.com生产数据库

Output

输出结果

  • Separate Clerk instances per environment
  • Environment-aware configuration
  • Webhook handling per environment
  • CI/CD pipeline configured
  • 每个环境拥有独立的Clerk实例
  • 支持环境感知的配置
  • 按环境处理Webhook
  • 已配置CI/CD流水线

Best Practices

最佳实践

  1. Never share keys between environments
  2. Use test keys for non-production
  3. Validate key/environment match at startup
  4. Separate webhook secrets per environment
  5. Isolate user data by environment
  1. 切勿在环境之间共享密钥
  2. 非生产环境使用测试密钥
  3. 启动时验证密钥与环境的匹配性
  4. 为每个环境配置独立的Webhook密钥
  5. 按环境隔离用户数据

Error Handling

错误处理

ErrorCauseSolution
Wrong environment keysMisconfigurationValidate at startup
Webhook signature failsWrong secretCheck env-specific secret
User not foundEnv mismatchCheck environment isolation
错误原因解决方案
环境密钥错误配置错误启动时进行验证
Webhook签名验证失败密钥错误检查对应环境的密钥
用户未找到环境不匹配检查环境隔离配置

Resources

参考资源

Next Steps

后续步骤

Proceed to
clerk-observability
for monitoring and logging.
继续执行
clerk-observability
进行监控与日志配置。