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".
85
83%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
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.
// 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;
}// 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;
}
}# .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# 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"// 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();# .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.
70e9fa4
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.