--- name: customerio-multi-env-setup description: 'Configure Customer.io multi-environment setup with workspace isolation. Use when setting up dev/staging/prod workspaces, environment-aware clients, or Kubernetes config overlays. Trigger: "customer.io environments", "customer.io staging", "customer.io dev prod", "customer.io workspace isolation". ' allowed-tools: Read, Write, Edit, Bash(npm:*), Bash(kubectl:*), Glob, Grep version: 1.14.0 license: MIT author: Jeremy Longshore tags: - saas - customer-io - multi-environment - kubernetes compatibility: Designed for Claude Code --- # Customer.io Multi-Environment Setup ## Output - Isolated development, staging, and production workspaces with separate scoped credentials, segments, and delivery controls. - A promotion receipt with synthetic validation, owner approval, and a rollback path that avoids production-recipient experiments. ## Examples Promote a campaign template by validating it against synthetic profiles in development, then staging, while keeping production workspace identifiers and secrets distinct. If staging delivery or consent checks fail, stop promotion and restore the previous approved template rather than editing the production campaign in place. ## Overview Configure isolated Customer.io environments for dev, staging, and production: separate workspaces per environment, typed configuration with validation, environment-aware client wrappers, Kubernetes ConfigMap overlays, and data isolation verification. ## Prerequisites - Customer.io account with multiple workspaces (create at fly.customer.io) - Environment variable management (dotenv, secrets manager) - CI/CD pipeline for per-environment deployment ## Workspace Strategy | Environment | Workspace Name | Purpose | Dry Run | Data | |-------------|---------------|---------|---------|------| | Local dev | `myapp-dev` | Individual developer testing | Optional | Fake/test data | | CI | `myapp-ci` | Automated test runs | No | Auto-cleaned test data | | Staging | `myapp-staging` | Pre-production validation | No | Subset of real data | | Production | `myapp-prod` | Live users | No | Real user data | Each workspace has its own Site ID, Track API Key, and App API Key. Create workspaces at Settings > Workspace Settings. ## Instructions ### Step 1: Typed Environment Configuration ```typescript // config/customerio.ts import { RegionUS, RegionEU } from "customerio-node"; type CioEnvironment = "development" | "ci" | "staging" | "production"; interface CioEnvConfig { siteId: string; trackApiKey: string; appApiKey: string; region: typeof RegionUS | typeof RegionEU; dryRun: boolean; logLevel: "debug" | "info" | "warn" | "error"; eventPrefix: string; // Prefix events in non-prod to prevent confusion } function validateConfig(config: CioEnvConfig, env: CioEnvironment): void { if (!config.siteId) throw new Error(`Missing CUSTOMERIO_SITE_ID for ${env}`); if (!config.trackApiKey) throw new Error(`Missing CUSTOMERIO_TRACK_API_KEY for ${env}`); if (env === "production" && config.dryRun) { throw new Error("Production cannot be in dry-run mode"); } if (env === "production" && config.eventPrefix) { throw new Error("Production must not use event prefix"); } } export function loadCioConfig(): CioEnvConfig { const env = (process.env.NODE_ENV ?? "development") as CioEnvironment; const region = process.env.CUSTOMERIO_REGION === "eu" ? RegionEU : RegionUS; const config: CioEnvConfig = { siteId: process.env.CUSTOMERIO_SITE_ID ?? "", trackApiKey: process.env.CUSTOMERIO_TRACK_API_KEY ?? "", appApiKey: process.env.CUSTOMERIO_APP_API_KEY ?? "", region, dryRun: process.env.CUSTOMERIO_DRY_RUN === "true", logLevel: (process.env.CUSTOMERIO_LOG_LEVEL as any) ?? (env === "production" ? "warn" : "debug"), eventPrefix: process.env.CUSTOMERIO_EVENT_PREFIX ?? (env === "production" ? "" : `${env}_`), }; validateConfig(config, env); return config; } ``` ### Step 2: Environment-Aware Client ```typescript // lib/customerio-env.ts import { TrackClient, APIClient } from "customerio-node"; import { loadCioConfig } from "../config/customerio"; const config = loadCioConfig(); export class EnvAwareCioClient { private track: TrackClient | null; private app: APIClient | null; constructor() { if (config.dryRun) { this.track = null; this.app = null; } else { this.track = new TrackClient(config.siteId, config.trackApiKey, { region: config.region, }); this.app = config.appApiKey ? new APIClient(config.appApiKey, { region: config.region }) : null; } } async identify(userId: string, attrs: Record): Promise { const prefixedId = config.eventPrefix ? `${config.eventPrefix}${userId}` : userId; // Tag with environment for debugging const envAttrs = { ...attrs, _cio_env: process.env.NODE_ENV, }; if (config.dryRun) { if (config.logLevel === "debug") { console.log(`[CIO DRY RUN] identify: ${prefixedId}`, envAttrs); } return; } await this.track!.identify(prefixedId, envAttrs); } async track(userId: string, name: string, data?: Record): Promise { const prefixedId = config.eventPrefix ? `${config.eventPrefix}${userId}` : userId; const prefixedName = config.eventPrefix ? `${config.eventPrefix}${name}` : name; if (config.dryRun) { if (config.logLevel === "debug") { console.log(`[CIO DRY RUN] track: ${prefixedId} ${prefixedName}`, data); } return; } await this.track!.track(prefixedId, { name: prefixedName, data }); } getAppClient(): APIClient { if (!this.app) { throw new Error("App API not available (dry-run or missing key)"); } return this.app; } } ``` ### Step 3: Environment Files ```bash # .env.development NODE_ENV=development CUSTOMERIO_SITE_ID=dev-workspace-site-id CUSTOMERIO_TRACK_API_KEY=dev-track-key CUSTOMERIO_APP_API_KEY=dev-app-key CUSTOMERIO_REGION=us CUSTOMERIO_DRY_RUN=false CUSTOMERIO_EVENT_PREFIX=dev_ CUSTOMERIO_LOG_LEVEL=debug # .env.staging NODE_ENV=staging CUSTOMERIO_SITE_ID=staging-workspace-site-id CUSTOMERIO_TRACK_API_KEY=staging-track-key CUSTOMERIO_APP_API_KEY=staging-app-key CUSTOMERIO_REGION=us CUSTOMERIO_DRY_RUN=false CUSTOMERIO_EVENT_PREFIX=staging_ CUSTOMERIO_LOG_LEVEL=info # .env.production (or use secrets manager) NODE_ENV=production CUSTOMERIO_SITE_ID=prod-workspace-site-id CUSTOMERIO_TRACK_API_KEY=prod-track-key CUSTOMERIO_APP_API_KEY=prod-app-key CUSTOMERIO_REGION=us CUSTOMERIO_DRY_RUN=false CUSTOMERIO_EVENT_PREFIX= CUSTOMERIO_LOG_LEVEL=warn ``` ### Step 4: Kubernetes ConfigMap Overlays ```yaml # k8s/base/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: customerio-config data: CUSTOMERIO_REGION: "us" CUSTOMERIO_LOG_LEVEL: "info" --- # k8s/overlays/development/configmap-patch.yaml apiVersion: v1 kind: ConfigMap metadata: name: customerio-config data: CUSTOMERIO_DRY_RUN: "true" CUSTOMERIO_EVENT_PREFIX: "dev_" CUSTOMERIO_LOG_LEVEL: "debug" --- # k8s/overlays/staging/configmap-patch.yaml apiVersion: v1 kind: ConfigMap metadata: name: customerio-config data: CUSTOMERIO_DRY_RUN: "false" CUSTOMERIO_EVENT_PREFIX: "staging_" CUSTOMERIO_LOG_LEVEL: "info" --- # k8s/overlays/production/configmap-patch.yaml apiVersion: v1 kind: ConfigMap metadata: name: customerio-config data: CUSTOMERIO_DRY_RUN: "false" CUSTOMERIO_EVENT_PREFIX: "" CUSTOMERIO_LOG_LEVEL: "warn" ``` ### Step 5: Data Isolation Verification ```typescript // scripts/verify-isolation.ts import { TrackClient, RegionUS } from "customerio-node"; async function verifyIsolation() { const envs = ["development", "staging", "production"]; const testId = `isolation-test-${Date.now()}`; for (const env of envs) { const siteId = process.env[`CIO_${env.toUpperCase()}_SITE_ID`]; const apiKey = process.env[`CIO_${env.toUpperCase()}_TRACK_KEY`]; if (!siteId || !apiKey) { console.log(`[SKIP] ${env}: credentials not configured`); continue; } const client = new TrackClient(siteId, apiKey, { region: RegionUS }); try { await client.identify(testId, { email: `${testId}@isolation-test.example.com`, _test_env: env, }); console.log(`[OK] ${env}: identify succeeded (separate workspace)`); // Clean up await client.suppress(testId); await client.destroy(testId); } catch (err: any) { console.log(`[FAIL] ${env}: ${err.statusCode} ${err.message}`); } } } verifyIsolation(); ``` ### Step 6: CI/CD Environment Promotion ```yaml # .github/workflows/promote.yml name: Promote to Environment on: workflow_dispatch: inputs: target_env: description: "Target environment" required: true type: choice options: [staging, production] jobs: promote: runs-on: ubuntu-latest environment: ${{ inputs.target_env }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 20 } - run: npm ci - name: Smoke test target environment env: CUSTOMERIO_SITE_ID: ${{ secrets.CIO_SITE_ID }} CUSTOMERIO_TRACK_API_KEY: ${{ secrets.CIO_TRACK_API_KEY }} run: npx tsx scripts/verify-customerio.ts - name: Deploy run: echo "Deploy to ${{ inputs.target_env }}" ``` ## Error Handling | Issue | Solution | |-------|----------| | Wrong workspace credentials | Config validation throws on startup — check error message | | Cross-env data leak | Event prefix prevents accidental production triggers | | Production in dry-run | Config validator explicitly blocks this combination | | Missing env-specific secret | Kubernetes ExternalSecrets or CI secret scoping | ## Resources - [Customer.io Workspaces](https://docs.customer.io/accounts-and-workspaces/managing-credentials/) - [Account Regions](https://docs.customer.io/accounts-and-workspaces/data-centers/) ## Next Steps After multi-env setup, proceed to `customerio-observability` for monitoring.