Customer.io SDK Patterns
Overview
Production-ready patterns for customerio-node: type-safe wrappers with enum-constrained events, retry with exponential backoff, event batching for high-volume scenarios, and singleton lifecycle management.
Prerequisites
customerio-node installed
- TypeScript project (recommended for type-safe patterns)
- Understanding of your event taxonomy
Instructions
Pattern 1: Type-Safe Client Wrapper
// lib/customerio-typed.ts
import { TrackClient, RegionUS, RegionEU } from "customerio-node";
// Define your event taxonomy as a union type
type CioEvent =
| { name: "signed_up"; data: { method: string; source?: string } }
| { name: "plan_changed"; data: { from: string; to: string; mrr: number } }
| { name: "feature_used"; data: { feature: string; duration_ms?: number } }
| { name: "checkout_completed"; data: { order_id: string; total: number; items: number } }
| { name: "subscription_cancelled"; data: { reason: string; feedback?: string } };
// Define user attributes with strict types
interface CioUserAttributes {
email: string;
first_name?: string;
last_name?: string;
plan?: "free" | "starter" | "pro" | "enterprise";
company?: string;
created_at?: number; // Unix seconds
last_seen_at?: number; // Unix seconds
[key: string]: unknown; // Allow additional attributes
}
export class TypedCioClient {
private client: TrackClient;
constructor(siteId: string, apiKey: string, region: "us" | "eu" = "us") {
this.client = new TrackClient(siteId, apiKey, {
region: region === "eu" ? RegionEU : RegionUS,
});
}
async identify(userId: string, attributes: CioUserAttributes): Promise<void> {
await this.client.identify(userId, {
...attributes,
last_seen_at: Math.floor(Date.now() / 1000),
});
}
async track(userId: string, event: CioEvent): Promise<void> {
await this.client.track(userId, {
name: event.name,
data: { ...event.data, tracked_at: Math.floor(Date.now() / 1000) },
});
}
async suppress(userId: string): Promise<void> {
await this.client.suppress(userId);
}
async destroy(userId: string): Promise<void> {
await this.client.destroy(userId);
}
}
Pattern 2: Retry with Exponential Backoff
// lib/customerio-retry.ts
interface RetryOptions {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
jitterFactor: number; // 0 to 1
}
const DEFAULT_RETRY: RetryOptions = {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 30000,
jitterFactor: 0.3,
};
async function withRetry<T>(
fn: () => Promise<T>,
opts: RetryOptions = DEFAULT_RETRY
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
try {
return await fn();
} catch (err: any) {
lastError = err;
const statusCode = err.statusCode ?? err.status;
// Don't retry client errors (except 429 rate limit)
if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) {
throw err;
}
if (attempt === opts.maxRetries) break;
// Exponential backoff with jitter
const delay = Math.min(
opts.baseDelayMs * Math.pow(2, attempt),
opts.maxDelayMs
);
const jitter = delay * opts.jitterFactor * Math.random();
await new Promise((r) => setTimeout(r, delay + jitter));
}
}
throw lastError;
}
// Usage with Customer.io client
import { TrackClient, RegionUS } from "customerio-node";
const cio = new TrackClient(
process.env.CUSTOMERIO_SITE_ID!,
process.env.CUSTOMERIO_TRACK_API_KEY!,
{ region: RegionUS }
);
// Wrap any operation with retry
await withRetry(() =>
cio.identify("user-123", { email: "user@example.com" })
);
await withRetry(() =>
cio.track("user-123", { name: "page_viewed", data: { url: "/pricing" } })
);
Pattern 3: Event Queue with Batching
// lib/customerio-batch.ts
import { TrackClient, RegionUS } from "customerio-node";
interface QueuedEvent {
userId: string;
name: string;
data?: Record<string, any>;
}
export class CioBatchTracker {
private queue: QueuedEvent[] = [];
private timer: NodeJS.Timeout | null = null;
private client: TrackClient;
constructor(
private readonly batchSize: number = 50,
private readonly flushIntervalMs: number = 5000
) {
this.client = new TrackClient(
process.env.CUSTOMERIO_SITE_ID!,
process.env.CUSTOMERIO_TRACK_API_KEY!,
{ region: RegionUS }
);
this.startTimer();
}
enqueue(userId: string, name: string, data?: Record<string, any>): void {
this.queue.push({ userId, name, data });
if (this.queue.length >= this.batchSize) {
this.flush();
}
}
async flush(): Promise<void> {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, this.batchSize);
const concurrency = 10;
for (let i = 0; i < batch.length; i += concurrency) {
const chunk = batch.slice(i, i + concurrency);
await Promise.allSettled(
chunk.map((event) =>
this.client.track(event.userId, {
name: event.name,
data: event.data,
})
)
);
}
}
private startTimer(): void {
this.timer = setInterval(() => this.flush(), this.flushIntervalMs);
}
async shutdown(): Promise<void> {
if (this.timer) clearInterval(this.timer);
await this.flush();
}
}
// Usage
const tracker = new CioBatchTracker(50, 5000);
// Non-blocking — events are queued and flushed automatically
tracker.enqueue("user-1", "page_viewed", { url: "/home" });
tracker.enqueue("user-2", "button_clicked", { button: "cta" });
// On process exit
process.on("SIGTERM", async () => {
await tracker.shutdown();
process.exit(0);
});
Pattern 4: Singleton with Validation
// lib/customerio-singleton.ts
import { TrackClient, APIClient, RegionUS, RegionEU } from "customerio-node";
class CioClientFactory {
private static trackInstance: TrackClient | null = null;
private static appInstance: APIClient | null = null;
static getTrackClient(): TrackClient {
if (!this.trackInstance) {
const siteId = process.env.CUSTOMERIO_SITE_ID;
const apiKey = process.env.CUSTOMERIO_TRACK_API_KEY;
if (!siteId || !apiKey) {
throw new Error(
"Missing CUSTOMERIO_SITE_ID or CUSTOMERIO_TRACK_API_KEY. " +
"Set these in your environment or .env file."
);
}
const region = process.env.CUSTOMERIO_REGION === "eu" ? RegionEU : RegionUS;
this.trackInstance = new TrackClient(siteId, apiKey, { region });
}
return this.trackInstance;
}
static getAppClient(): APIClient {
if (!this.appInstance) {
const appKey = process.env.CUSTOMERIO_APP_API_KEY;
if (!appKey) {
throw new Error(
"Missing CUSTOMERIO_APP_API_KEY. " +
"Set this in your environment or .env file."
);
}
const region = process.env.CUSTOMERIO_REGION === "eu" ? RegionEU : RegionUS;
this.appInstance = new APIClient(appKey, { region });
}
return this.appInstance;
}
/** Reset for testing */
static reset(): void {
this.trackInstance = null;
this.appInstance = null;
}
}
// Usage — same instance everywhere
const cio = CioClientFactory.getTrackClient();
const api = CioClientFactory.getAppClient();
Pattern Summary
| Pattern |
When to Use |
Key Benefit |
| Typed Client |
Always |
Compile-time safety on events + attributes |
| Retry + Backoff |
Production API calls |
Handles transient 5xx and 429 errors |
| Batch Queue |
High-volume tracking (>100 events/sec) |
Reduces connection overhead, respects rate limits |
| Singleton Factory |
Multi-module apps |
Prevents connection leaks, validates config once |
Error Handling
| Error |
Cause |
Solution |
| Type mismatch |
Wrong event data shape |
Use TypeScript union types for events |
| Queue memory growth |
Events produced faster than flushed |
Lower batchSize, increase flush frequency |
| Retry exhausted (3x) |
Persistent API failure |
Check credentials, Customer.io status page |
| Singleton null credentials |
Env vars not loaded |
Ensure dotenv loads before client creation |
Resources
Next Steps
After implementing patterns, proceed to customerio-primary-workflow for messaging workflows.
1---2name: customerio-sdk-patterns3description: |4 Apply production-ready Customer.io SDK patterns.5 Use when implementing typed clients, retry logic, event batching,6 or singleton management for customerio-node.7 Trigger: "customer.io best practices", "customer.io patterns",8 "production customer.io", "customer.io architecture", "customer.io singleton".9allowed-tools: Read, Write, Edit, Bash(npm:*), Bash(npx:*), Glob, Grep10version: 1.0.011license: MIT12author: Jeremy Longshore <jeremy@intentsolutions.io>13compatible-with: claude-code, codex, openclaw14tags: [saas, customer-io, sdk, patterns, typescript]1516---1718# Customer.io SDK Patterns1920## Overview2122Production-ready patterns for `customerio-node`: type-safe wrappers with enum-constrained events, retry with exponential backoff, event batching for high-volume scenarios, and singleton lifecycle management.2324## Prerequisites2526- `customerio-node` installed27- TypeScript project (recommended for type-safe patterns)28- Understanding of your event taxonomy2930## Instructions3132### Pattern 1: Type-Safe Client Wrapper3334```typescript35// lib/customerio-typed.ts36import { TrackClient, RegionUS, RegionEU } from "customerio-node";3738// Define your event taxonomy as a union type39type CioEvent =40 | { name: "signed_up"; data: { method: string; source?: string } }41 | { name: "plan_changed"; data: { from: string; to: string; mrr: number } }42 | { name: "feature_used"; data: { feature: string; duration_ms?: number } }43 | { name: "checkout_completed"; data: { order_id: string; total: number; items: number } }44 | { name: "subscription_cancelled"; data: { reason: string; feedback?: string } };4546// Define user attributes with strict types47interface CioUserAttributes {48 email: string;49 first_name?: string;50 last_name?: string;51 plan?: "free" | "starter" | "pro" | "enterprise";52 company?: string;53 created_at?: number; // Unix seconds54 last_seen_at?: number; // Unix seconds55 [key: string]: unknown; // Allow additional attributes56}5758export class TypedCioClient {59 private client: TrackClient;6061 constructor(siteId: string, apiKey: string, region: "us" | "eu" = "us") {62 this.client = new TrackClient(siteId, apiKey, {63 region: region === "eu" ? RegionEU : RegionUS,64 });65 }6667 async identify(userId: string, attributes: CioUserAttributes): Promise<void> {68 await this.client.identify(userId, {69 ...attributes,70 last_seen_at: Math.floor(Date.now() / 1000),71 });72 }7374 async track(userId: string, event: CioEvent): Promise<void> {75 await this.client.track(userId, {76 name: event.name,77 data: { ...event.data, tracked_at: Math.floor(Date.now() / 1000) },78 });79 }8081 async suppress(userId: string): Promise<void> {82 await this.client.suppress(userId);83 }8485 async destroy(userId: string): Promise<void> {86 await this.client.destroy(userId);87 }88}89```9091### Pattern 2: Retry with Exponential Backoff9293```typescript94// lib/customerio-retry.ts95interface RetryOptions {96 maxRetries: number;97 baseDelayMs: number;98 maxDelayMs: number;99 jitterFactor: number; // 0 to 1100}101102const DEFAULT_RETRY: RetryOptions = {103 maxRetries: 3,104 baseDelayMs: 1000,105 maxDelayMs: 30000,106 jitterFactor: 0.3,107};108109async function withRetry<T>(110 fn: () => Promise<T>,111 opts: RetryOptions = DEFAULT_RETRY112): Promise<T> {113 let lastError: Error | undefined;114115 for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {116 try {117 return await fn();118 } catch (err: any) {119 lastError = err;120 const statusCode = err.statusCode ?? err.status;121122 // Don't retry client errors (except 429 rate limit)123 if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) {124 throw err;125 }126127 if (attempt === opts.maxRetries) break;128129 // Exponential backoff with jitter130 const delay = Math.min(131 opts.baseDelayMs * Math.pow(2, attempt),132 opts.maxDelayMs133 );134 const jitter = delay * opts.jitterFactor * Math.random();135 await new Promise((r) => setTimeout(r, delay + jitter));136 }137 }138 throw lastError;139}140141// Usage with Customer.io client142import { TrackClient, RegionUS } from "customerio-node";143144const cio = new TrackClient(145 process.env.CUSTOMERIO_SITE_ID!,146 process.env.CUSTOMERIO_TRACK_API_KEY!,147 { region: RegionUS }148);149150// Wrap any operation with retry151await withRetry(() =>152 cio.identify("user-123", { email: "user@example.com" })153);154155await withRetry(() =>156 cio.track("user-123", { name: "page_viewed", data: { url: "/pricing" } })157);158```159160### Pattern 3: Event Queue with Batching161162```typescript163// lib/customerio-batch.ts164import { TrackClient, RegionUS } from "customerio-node";165166interface QueuedEvent {167 userId: string;168 name: string;169 data?: Record<string, any>;170}171172export class CioBatchTracker {173 private queue: QueuedEvent[] = [];174 private timer: NodeJS.Timeout | null = null;175 private client: TrackClient;176177 constructor(178 private readonly batchSize: number = 50,179 private readonly flushIntervalMs: number = 5000180 ) {181 this.client = new TrackClient(182 process.env.CUSTOMERIO_SITE_ID!,183 process.env.CUSTOMERIO_TRACK_API_KEY!,184 { region: RegionUS }185 );186 this.startTimer();187 }188189 enqueue(userId: string, name: string, data?: Record<string, any>): void {190 this.queue.push({ userId, name, data });191 if (this.queue.length >= this.batchSize) {192 this.flush();193 }194 }195196 async flush(): Promise<void> {197 if (this.queue.length === 0) return;198 const batch = this.queue.splice(0, this.batchSize);199 const concurrency = 10;200201 for (let i = 0; i < batch.length; i += concurrency) {202 const chunk = batch.slice(i, i + concurrency);203 await Promise.allSettled(204 chunk.map((event) =>205 this.client.track(event.userId, {206 name: event.name,207 data: event.data,208 })209 )210 );211 }212 }213214 private startTimer(): void {215 this.timer = setInterval(() => this.flush(), this.flushIntervalMs);216 }217218 async shutdown(): Promise<void> {219 if (this.timer) clearInterval(this.timer);220 await this.flush();221 }222}223224// Usage225const tracker = new CioBatchTracker(50, 5000);226227// Non-blocking — events are queued and flushed automatically228tracker.enqueue("user-1", "page_viewed", { url: "/home" });229tracker.enqueue("user-2", "button_clicked", { button: "cta" });230231// On process exit232process.on("SIGTERM", async () => {233 await tracker.shutdown();234 process.exit(0);235});236```237238### Pattern 4: Singleton with Validation239240```typescript241// lib/customerio-singleton.ts242import { TrackClient, APIClient, RegionUS, RegionEU } from "customerio-node";243244class CioClientFactory {245 private static trackInstance: TrackClient | null = null;246 private static appInstance: APIClient | null = null;247248 static getTrackClient(): TrackClient {249 if (!this.trackInstance) {250 const siteId = process.env.CUSTOMERIO_SITE_ID;251 const apiKey = process.env.CUSTOMERIO_TRACK_API_KEY;252 if (!siteId || !apiKey) {253 throw new Error(254 "Missing CUSTOMERIO_SITE_ID or CUSTOMERIO_TRACK_API_KEY. " +255 "Set these in your environment or .env file."256 );257 }258 const region = process.env.CUSTOMERIO_REGION === "eu" ? RegionEU : RegionUS;259 this.trackInstance = new TrackClient(siteId, apiKey, { region });260 }261 return this.trackInstance;262 }263264 static getAppClient(): APIClient {265 if (!this.appInstance) {266 const appKey = process.env.CUSTOMERIO_APP_API_KEY;267 if (!appKey) {268 throw new Error(269 "Missing CUSTOMERIO_APP_API_KEY. " +270 "Set this in your environment or .env file."271 );272 }273 const region = process.env.CUSTOMERIO_REGION === "eu" ? RegionEU : RegionUS;274 this.appInstance = new APIClient(appKey, { region });275 }276 return this.appInstance;277 }278279 /** Reset for testing */280 static reset(): void {281 this.trackInstance = null;282 this.appInstance = null;283 }284}285286// Usage — same instance everywhere287const cio = CioClientFactory.getTrackClient();288const api = CioClientFactory.getAppClient();289```290291## Pattern Summary292293| Pattern | When to Use | Key Benefit |294|---------|------------|-------------|295| Typed Client | Always | Compile-time safety on events + attributes |296| Retry + Backoff | Production API calls | Handles transient 5xx and 429 errors |297| Batch Queue | High-volume tracking (>100 events/sec) | Reduces connection overhead, respects rate limits |298| Singleton Factory | Multi-module apps | Prevents connection leaks, validates config once |299300## Error Handling301302| Error | Cause | Solution |303|-------|-------|----------|304| Type mismatch | Wrong event data shape | Use TypeScript union types for events |305| Queue memory growth | Events produced faster than flushed | Lower `batchSize`, increase flush frequency |306| Retry exhausted (3x) | Persistent API failure | Check credentials, Customer.io status page |307| Singleton null credentials | Env vars not loaded | Ensure `dotenv` loads before client creation |308309## Resources310311- [customerio-node SDK](https://github.com/customerio/customerio-node)312- [Track API Rate Limits](https://docs.customer.io/integrations/api/track/)313- [About Customer.io APIs](https://docs.customer.io/integrations/api/customerio-apis/)314315## Next Steps316317After implementing patterns, proceed to `customerio-primary-workflow` for messaging workflows.318