Skip to main content
Version: Next

Custom Providers

While CommandKit's built-in feature flag system is powerful for local flag management, you may want to integrate with external feature flag services like LaunchDarkly, Split, Unleash, or your own custom backend. CommandKit supports this through the provider pattern.

What are Flag Providers?

Flag providers are adapters that allow CommandKit to fetch feature flag configurations from external systems. When a provider is configured, your local decide function receives additional context from the external system, giving you the flexibility to combine local logic with remote configuration.

Setting Up a Provider

Step 1: Implement the FlagProvider Interface

src/providers/LaunchDarklyProvider.ts
import { FlagProvider, FlagConfiguration } from 'commandkit';

export class LaunchDarklyProvider implements FlagProvider {
private client: any; // LaunchDarkly SDK client

constructor(private apiKey: string) {}

async initialize(): Promise<void> {
// Initialize LaunchDarkly client
// this.client = LaunchDarkly.initialize(this.apiKey);
// await this.client.waitUntilReady();
console.log('LaunchDarkly provider initialized');
}

async getFlag(key: string, context?: any): Promise<FlagConfiguration | null> {
try {
// Fetch flag from LaunchDarkly
// const variation = await this.client.variation(key, context, false);
// const flagDetail = await this.client.variationDetail(key, context, false);

// Return provider configuration
return {
enabled: true, // Whether the flag is enabled
percentage: 75, // Optional: percentage rollout
config: {
// Custom configuration from LaunchDarkly
colors: ['#ff0000', '#00ff00'],
feature: 'enhanced-ui',
},
};
} catch (error) {
console.error(`LaunchDarkly flag evaluation failed for ${key}:`, error);
return null;
}
}

async hasFlag(key: string): Promise<boolean> {
// Check if flag exists in LaunchDarkly
return true;
}

async destroy(): Promise<void> {
// Clean up LaunchDarkly client
// await this.client.close();
}
}

Step 2: Configure the Global Provider

src/app.ts
import { setFlagProvider } from 'commandkit';
import { LaunchDarklyProvider } from './providers/LaunchDarklyProvider';

const provider = new LaunchDarklyProvider(process.env.LAUNCHDARKLY_API_KEY!);

// Initialize and set the global provider
await provider.initialize();
setFlagProvider(provider);

Step 3: Use Provider Data in Your Flags

src/flags/embedColorFlag.ts
import { flag } from 'commandkit';
import murmurhash from 'murmurhash';

export const embedColorFlag = flag({
key: 'embed-color-flag',
description: 'Show different embed colors based on external configuration',
identify(ctx) {
const command = ctx.command;
const id = command?.interaction?.user.id ?? command?.message?.author.id;
return { id: id ?? null };
},
decide({ entities, provider }) {
// Local fallback when provider is unavailable
if (!provider) {
// Use local hashing logic
const hash = murmurhash.v3(entities.id || 'anonymous');
return hash % 100 < 50;
}

// Provider-based logic
if (!provider.enabled) {
return false; // Provider disabled this flag
}

// Use provider's percentage rollout
if (provider.percentage) {
const hash = murmurhash.v3(entities.id || 'anonymous');
const bucket = hash % 100;
return bucket < provider.percentage;
}

// Use provider's custom config
if (provider.config?.colors) {
return provider.config.colors[0]; // Return specific color
}

return provider.enabled;
},
});

Built-in JSON Provider

CommandKit includes a simple JSON-based provider for basic external configuration:

src/providers/config.ts
import { JsonFlagProvider, setFlagProvider } from 'commandkit';

const provider = new JsonFlagProvider({
'embed-color-flag': {
enabled: true,
percentage: 75,
config: {
colors: ['#ff0000', '#00ff00', '#0000ff'],
theme: 'dark',
},
},
'premium-features': {
enabled: true,
targeting: {
segments: ['premium_users', 'beta_testers'],
},
},
});

await provider.initialize();
setFlagProvider(provider);

Advanced Provider Examples

Database-backed Provider

src/providers/DatabaseProvider.ts
import { FlagProvider, FlagConfiguration } from 'commandkit';

export class DatabaseProvider implements FlagProvider {
constructor(private db: any) {}

async getFlag(key: string): Promise<FlagConfiguration | null> {
const result = await this.db.query(
'SELECT * FROM feature_flags WHERE key = ?',
[key],
);

if (!result.length) return null;

const flag = result[0];
return {
enabled: flag.enabled,
percentage: flag.rollout_percentage,
config: JSON.parse(flag.config || '{}'),
targeting: JSON.parse(flag.targeting || '{}'),
};
}

async hasFlag(key: string): Promise<boolean> {
const result = await this.db.query(
'SELECT 1 FROM feature_flags WHERE key = ?',
[key],
);
return result.length > 0;
}
}

Multi-source Provider

src/providers/MultiProvider.ts
import { FlagProvider, FlagConfiguration } from 'commandkit';

export class MultiProvider implements FlagProvider {
constructor(private providers: FlagProvider[]) {}

async getFlag(key: string, context?: any): Promise<FlagConfiguration | null> {
// Try providers in order, return first successful result
for (const provider of this.providers) {
try {
const result = await provider.getFlag(key, context);
if (result) return result;
} catch (error) {
console.warn(`Provider failed for ${key}:`, error);
continue;
}
}
return null;
}

async hasFlag(key: string): Promise<boolean> {
for (const provider of this.providers) {
if (await provider.hasFlag(key)) return true;
}
return false;
}
}

Best Practices

1. Always Provide Fallbacks

Your decide function should handle cases where the provider is unavailable:

decide({ entities, provider }) {
// Always check if provider data is available
if (!provider) {
// Local fallback logic
return defaultBehavior(entities);
}

// Provider-based logic
return providerBasedLogic(entities, provider);
}

2. Handle Provider Errors Gracefully

Providers may fail due to network issues, API limits, or configuration problems. CommandKit automatically catches these errors and continues with local evaluation.

3. Use Provider Configuration Wisely

The provider's config object can contain any data structure your external system provides:

decide({ entities, provider }) {
if (provider?.config?.experimentConfig) {
const experiment = provider.config.experimentConfig;
return evaluateExperiment(entities, experiment);
}

return defaultLogic(entities);
}

4. Disable Analytics When Needed

For high-frequency flags or privacy-sensitive scenarios, you can disable analytics:

export const highFrequencyFlag = flag({
key: 'rate-limiting-flag',
description: 'High frequency flag for rate limiting',
disableAnalytics: true, // Skip analytics tracking
decide({ entities }) {
return entities.requestCount > 100;
},
});

Provider Interface Reference

interface FlagProvider {
initialize?(): Promise<void>;
getFlag(key: string, context?: any): Promise<FlagConfiguration | null>;
hasFlag(key: string): Promise<boolean>;
destroy?(): Promise<void>;
}

interface FlagConfiguration {
enabled: boolean;
config?: Record<string, any>;
percentage?: number;
targeting?: {
segments?: string[];
rules?: Array<{
condition: string;
value: any;
}>;
};
}

The provider pattern makes CommandKit's feature flags extremely flexible while maintaining the simplicity of the core API. You can start with local flags and gradually migrate to external providers as your needs grow.