Skip to main content
CommandKit’s feature flag system allows you to dynamically control feature availability, perform A/B testing, and gradually roll out new functionality. It integrates seamlessly with external providers like LaunchDarkly, Split, or Unleash.

Creating Feature Flags

Basic Flag

import { flag } from 'commandkit';

const isNewFeatureEnabled = flag({
  key: 'new-feature',
  description: 'Enable the new feature',
  
  identify: (ctx) => ({
    userId: ctx.client.user.id,
    guildId: ctx.command?.guild?.id,
  }),
  
  decide: ({ entities }) => {
    // Simple boolean decision
    return true;
  },
});

// Use in commands
export const chatInput: ChatInputCommand = async (ctx) => {
  const enabled = await isNewFeatureEnabled();
  
  if (enabled) {
    await ctx.interaction.reply('New feature!');
  } else {
    await ctx.interaction.reply('Old feature');
  }
};
Source: packages/commandkit/src/flags/feature-flags.ts:410

Flag with Context

const isPremiumUser = flag<boolean, { userId: string; guildId: string }>({
  key: 'premium-user',
  description: 'Check if user has premium access',
  
  identify: (ctx) => ({
    userId: ctx.command?.interaction?.user.id || '',
    guildId: ctx.command?.guild?.id || '',
  }),
  
  decide: async ({ entities, provider }) => {
    // Check external provider first
    if (provider?.enabled) {
      return provider.enabled;
    }
    
    // Fallback to local logic
    const premium = await checkPremiumStatus(entities.userId);
    return premium;
  },
});
Source: packages/commandkit/src/flags/feature-flags.ts:64-91

Flag Definition

FeatureFlagDefinition Interface

interface FeatureFlagDefinition<R, Entity> {
  // Unique identifier for the flag
  key: string;
  
  // Optional description
  description?: string;
  
  // Extract entities from context
  identify?: (context: EvaluationContext) => MaybePromise<Entity>;
  
  // Decision logic
  decide: (data: {
    entities: Entity;
    provider?: FlagConfiguration | null;
  }) => MaybePromise<R>;
  
  // Disable analytics tracking
  disableAnalytics?: boolean;
}
Source: packages/commandkit/src/flags/feature-flags.ts:64

Key Components

key

Unique string identifier for the flag. Used for external provider lookups.

identify

Extracts entities from the execution context. These entities are used in the decide function.
identify: (ctx) => ({
  userId: ctx.command?.interaction?.user.id,
  guildId: ctx.command?.guild?.id,
  channelId: ctx.command?.channel?.id,
})

decide

Contains the flag evaluation logic. Receives identified entities and optional provider configuration.
decide: async ({ entities, provider }) => {
  // Use provider config if available
  if (provider?.enabled) return true;
  
  // Custom logic
  return entities.userId === 'admin-user-id';
}

Evaluation Context

Flags can access different contexts depending on where they’re evaluated.

Command Context

interface CommandFlagContext {
  client: Client<true>;
  commandkit: CommandKit;
  command: {
    interaction?: ChatInputCommandInteraction | AutocompleteInteraction;
    message?: Message;
    guild: Guild | null;
    channel: TextBasedChannel | null;
    command: LoadedCommand;
  };
  event: null;
}
Source: packages/commandkit/src/flags/feature-flags.ts:96

Event Context

interface EventFlagContext {
  client: Client<true>;
  commandkit: CommandKit;
  event: {
    data: ParsedEvent;
    event: string;
    namespace: string | null;
    arguments: any[];
    argumentsAs<E extends keyof ClientEvents>(event: E): ClientEvents[E];
  };
  command: null;
}
Source: packages/commandkit/src/flags/feature-flags.ts:150

Advanced Usage

Percentage Rollouts

const gradualRollout = flag({
  key: 'gradual-feature',
  
  identify: (ctx) => ({
    userId: ctx.command?.interaction?.user.id || '',
  }),
  
  decide: ({ entities, provider }) => {
    // Use provider percentage if available
    if (provider?.percentage) {
      const hash = hashString(entities.userId);
      return (hash % 100) < provider.percentage;
    }
    
    // Default 10% rollout
    const hash = hashString(entities.userId);
    return (hash % 100) < 10;
  },
});

Complex Decisions

interface BetaFeatureEntities {
  userId: string;
  guildId: string;
  memberCount: number;
}

const betaFeature = flag<boolean, BetaFeatureEntities>({
  key: 'beta-feature',
  
  identify: async (ctx) => {
    const guild = ctx.command?.guild;
    return {
      userId: ctx.command?.interaction?.user.id || '',
      guildId: guild?.id || '',
      memberCount: guild?.memberCount || 0,
    };
  },
  
  decide: async ({ entities, provider }) => {
    // Check multiple conditions
    if (entities.memberCount < 100) return true;
    if (provider?.config?.betaGuilds?.includes(entities.guildId)) return true;
    if (await isTestUser(entities.userId)) return true;
    
    return false;
  },
});

Custom Evaluation

Evaluate flags with custom context outside normal execution flow.
const customFlag = flag({
  key: 'custom-check',
  identify: (ctx) => ({ id: '1' }),
  decide: ({ entities }) => entities.id === '1',
});

// Custom evaluation
const result = await customFlag.run({
  identify: { id: '2', custom: 'data' }
});
Source: packages/commandkit/src/flags/feature-flags.ts:419

External Flag Providers

FlagProvider Interface

interface FlagProvider {
  // Initialize the provider
  initialize?(): MaybePromise<void>;
  
  // Get flag configuration
  getFlag(key: string, context?: any): MaybePromise<FlagConfiguration | null>;
  
  // Check if flag exists
  hasFlag(key: string): MaybePromise<boolean>;
  
  // Cleanup
  destroy?(): MaybePromise<void>;
}
Source: packages/commandkit/src/flags/FlagProvider.ts:8

FlagConfiguration

interface FlagConfiguration {
  // Whether flag is enabled
  enabled: boolean;
  
  // Additional config data
  config?: Record<string, any>;
  
  // Rollout percentage (0-100)
  percentage?: number;
  
  // Targeting rules
  targeting?: {
    segments?: string[];
    rules?: Array<{
      condition: string;
      value: any;
    }>;
  };
}
Source: packages/commandkit/src/flags/FlagProvider.ts:37

Setting a Provider

import { setFlagProvider } from 'commandkit';
import { LaunchDarklyProvider } from './providers/launchdarkly';

const provider = new LaunchDarklyProvider({
  sdkKey: process.env.LAUNCHDARKLY_KEY!,
});

await provider.initialize();
setFlagProvider(provider);
Source: packages/commandkit/src/flags/feature-flags.ts:27

JSON Provider Example

import { JsonFlagProvider } from 'commandkit';

const provider = new JsonFlagProvider({
  'new-feature': {
    enabled: true,
    percentage: 50,
  },
  'beta-feature': {
    enabled: false,
  },
});

setFlagProvider(provider);
Source: packages/commandkit/src/flags/FlagProvider.ts:68

Custom Provider

import { FlagProvider, FlagConfiguration } from 'commandkit';

class DatabaseFlagProvider implements FlagProvider {
  constructor(private db: Database) {}
  
  async initialize() {
    await this.db.connect();
  }
  
  async getFlag(key: string, context?: any): Promise<FlagConfiguration | null> {
    const flag = await this.db.flags.findOne({ key });
    
    if (!flag) return null;
    
    return {
      enabled: flag.enabled,
      config: flag.config,
      percentage: flag.rolloutPercentage,
    };
  }
  
  async hasFlag(key: string): Promise<boolean> {
    const count = await this.db.flags.count({ key });
    return count > 0;
  }
  
  async destroy() {
    await this.db.disconnect();
  }
}

// Use it
const provider = new DatabaseFlagProvider(db);
await provider.initialize();
setFlagProvider(provider);

Analytics

Flags automatically track metrics and decisions (unless disabled).

Tracked Events

  1. Feature Flag Metrics
    • Flag key
    • Identification time
    • Decision time
    • Provider usage
  2. Feature Flag Decisions
    • Flag key
    • Decision result
    • User/entity ID
    • Provider used
Source: packages/commandkit/src/flags/feature-flags.ts:369-398

Disable Analytics

const silentFlag = flag({
  key: 'no-tracking',
  disableAnalytics: true,
  
  identify: (ctx) => ({}),
  decide: () => true,
});
Source: packages/commandkit/src/flags/feature-flags.ts:88

Use Cases

Beta Features

const betaUI = flag({
  key: 'beta-ui',
  
  identify: (ctx) => ({
    userId: ctx.command?.interaction?.user.id,
  }),
  
  decide: async ({ entities }) => {
    const betaUsers = await getBetaUsers();
    return betaUsers.includes(entities.userId);
  },
});

export const chatInput: ChatInputCommand = async (ctx) => {
  const useBeta = await betaUI();
  
  if (useBeta) {
    await ctx.interaction.reply({ embeds: [newBetaEmbed()] });
  } else {
    await ctx.interaction.reply({ embeds: [standardEmbed()] });
  }
};

Guild-Specific Features

const premiumFeature = flag({
  key: 'premium-feature',
  
  identify: (ctx) => ({
    guildId: ctx.command?.guild?.id || '',
  }),
  
  decide: async ({ entities, provider }) => {
    // Check provider first
    if (provider?.config?.premiumGuilds) {
      return provider.config.premiumGuilds.includes(entities.guildId);
    }
    
    // Fallback to database
    return await isPremiumGuild(entities.guildId);
  },
});

A/B Testing

const abTestVariant = flag<'A' | 'B', { userId: string }>({
  key: 'ab-test',
  
  identify: (ctx) => ({
    userId: ctx.command?.interaction?.user.id || '',
  }),
  
  decide: ({ entities }) => {
    const hash = hashString(entities.userId);
    return (hash % 2) === 0 ? 'A' : 'B';
  },
});

export const chatInput: ChatInputCommand = async (ctx) => {
  const variant = await abTestVariant();
  
  if (variant === 'A') {
    await ctx.interaction.reply('Variant A');
  } else {
    await ctx.interaction.reply('Variant B');
  }
};

Maintenance Mode

const maintenanceMode = flag({
  key: 'maintenance-mode',
  
  identify: (ctx) => ({
    userId: ctx.command?.interaction?.user.id || '',
  }),
  
  decide: async ({ entities, provider }) => {
    // Check provider for global maintenance
    if (provider?.enabled) return true;
    
    // Allow admins during maintenance
    return !(await isAdmin(entities.userId));
  },
});

export const chatInput: ChatInputCommand = async (ctx) => {
  const inMaintenance = await maintenanceMode();
  
  if (inMaintenance) {
    await ctx.interaction.reply('Bot is under maintenance!');
    return;
  }
  
  // Normal command logic
};

Best Practices

  1. Use Descriptive Keys: Use clear, hierarchical keys like feature.ui.new-buttons
  2. Cache Provider Calls: External providers should cache results to avoid rate limits
  3. Fallback Logic: Always provide fallback logic in decide if provider fails
  4. Type Safety: Use TypeScript generics for return types and entities
const typedFlag = flag<boolean, { id: string }>({
  key: 'typed',
  identify: (ctx) => ({ id: '1' }),
  decide: ({ entities }) => entities.id === '1',
});
  1. Monitor Performance: Use analytics to track flag evaluation time

Build docs developers (and LLMs) love