Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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".
.claude/skills/jeremylongshore-customerio-multi-env-setup/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 36% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 86% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 96% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 73% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 140% | 0% |
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.
| 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.
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; }
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<string, any>): Promise<void> { 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<string, any>): Promise<void> { 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; } }
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
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"
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();
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 }}"
| 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 |
After multi-env setup, proceed to customerio-observability for monitoring.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 18,755 | 13,436 | -28% | 1 | 1 | 0% | 4,292 | 5,845 | +36% | 0 | 0 | — |
case-02 | fail→pass | 12,316 | 9,371 | -24% | 1 | 1 | 0% | 2,673 | 4,979 | +86% | 0 | 0 | — |
case-03 | fail→fail | 21,966 | 19,695 | -10% | 1 | 1 | 0% | 4,647 | 7,002 | +51% | 0 | 0 | — |
case-04 | fail→fail | 14,908 | 17,808 | +19% | 1 | 1 | 0% | 3,007 | 6,244 | +108% | 0 | 0 | — |
case-05 | fail→pass | 11,187 | 8,247 | -26% | 1 | 1 | 0% | 2,135 | 4,191 | +96% | 0 | 0 | — |
case-06 | fail→pass | 8,799 | 1,727 | -80% | 1 | 1 | 0% | 1,699 | 2,942 | +73% | 0 | 0 | — |
case-07 | pass→pass | 11,069 | 6,634 | -40% | 1 | 1 | 0% | 2,276 | 3,934 | +73% | 0 | 0 | — |
case-08 | fail→pass | 7,195 | 3,057 | -58% | 1 | 1 | 0% | 1,346 | 3,235 | +140% | 0 | 0 | — |
case-09 | fail→pass | 11,164 | 6,306 | -44% | 1 | 1 | 0% | 2,043 | 3,707 | +81% | 0 | 0 | — |
case-10 | pass→pass | 12,221 | 8,950 | -27% | 1 | 1 | 0% | 2,182 | 4,433 | +103% | 0 | 0 | — |
case-11 | pass→pass | 7,379 | 2,603 | -65% | 1 | 1 | 0% | 1,350 | 3,132 | +132% | 0 | 0 | — |
case-12 | pass→pass | 12,507 | 9,100 | -27% | 1 | 1 | 0% | 2,483 | 4,397 | +77% | 0 | 0 | — |
case-13 | fail→pass | 18,113 | 3,280 | -82% | 1 | 1 | 0% | 1,032 | 3,270 | +217% | 0 | 0 | — |
case-14 | fail→pass | 8,816 | 3,783 | -57% | 1 | 1 | 0% | 1,783 | 3,419 | +92% | 0 | 0 | — |
case-15 | fail→pass | 14,384 | 12,250 | -15% | 1 | 1 | 0% | 2,524 | 5,131 | +103% | 0 | 0 | — |
case-16 | pass→pass | 12,533 | 7,114 | -43% | 1 | 1 | 0% | 2,282 | 3,877 | +70% | 0 | 0 | — |
case-17 | pass→pass | 14,271 | 7,418 | -48% | 1 | 1 | 0% | 2,384 | 4,142 | +74% | 0 | 0 | — |
case-18 | fail→pass | 12,650 | 4,736 | -63% | 1 | 1 | 0% | 2,405 | 3,542 | +47% | 0 | 0 | — |
case-19 | pass→pass | 7,245 | 2,243 | -69% | 1 | 1 | 0% | 1,180 | 2,977 | +152% | 0 | 0 | — |
case-20 | pass→pass | 12,359 | 11,976 | -3% | 1 | 1 | 0% | 2,399 | 5,053 | +111% | 0 | 0 | — |
case-21 | pass→pass | 12,525 | 13,823 | +10% | 1 | 1 | 0% | 2,747 | 5,691 | +107% | 0 | 0 | — |
case-22 | pass→pass | 11,495 | 12,045 | +5% | 1 | 1 | 0% | 2,491 | 5,220 | +110% | 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. 22 cases were attempted, and 21 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 +45 percentage points is the difference between those two pass rates over the 21 comparable cases.
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.