Skip to content

Feature Flags

Evaluate feature flags in real-time with targeting, percentage rollouts, and OpenFeature compatibility.

Overview

Feature Flags allow you to control feature releases and run experiments:

  • Boolean flags - Simple on/off toggles
  • String flags - A/B testing with variants
  • Number flags - Numeric configuration values
  • JSON flags - Complex configuration objects
  • Targeting - User-based targeting with rules
  • Percentage rollouts - Gradual feature releases
  • OpenFeature - Vendor-neutral SDK integration

Quick Start

typescript
import { FlagsClient } from '@kitbase/flags';

const flags = new FlagsClient({
  token: '<YOUR_API_KEY>',
});

// Simple boolean check
const isEnabled = await flags.getBooleanValue('dark-mode', false, {
  targetingKey: 'user-123',
  plan: 'premium',
});

if (isEnabled) {
  enableDarkMode();
}
dart
import 'package:kitbase/flags.dart';

final flags = KitbaseFlags(token: '<YOUR_API_KEY>');

// Simple boolean check
final isEnabled = await flags.getBooleanValue(
  'dark-mode',
  false,
  context: EvaluationContext(
    targetingKey: 'user-123',
    attributes: {'plan': 'premium'},
  ),
);

if (isEnabled) {
  enableDarkMode();
}

flags.close();
php
use Kitbase\Flags\Flags;
use Kitbase\Flags\FlagsConfig;
use Kitbase\Flags\EvaluationContext;

$flags = new Flags(new FlagsConfig(
    token: '<YOUR_API_KEY>',
));

// Simple boolean check
$isEnabled = $flags->getBooleanValue('dark-mode', false, new EvaluationContext(
    targetingKey: 'user-123',
    attributes: ['plan' => 'premium'],
));

if ($isEnabled) {
    enableDarkMode();
}

Flag Value Types

Kitbase supports four flag value types:

TypeDescriptionExample
booleanOn/off togglestrue, false
stringText values, variants"control", "treatment-a"
numberNumeric values42, 3.14
jsonComplex objects{"limit": 100, "tier": "pro"}

Boolean Flags

typescript
// Get just the value
const isEnabled = await flags.getBooleanValue('dark-mode', false, context);

// Get value with resolution details
const result = await flags.getBooleanDetails('dark-mode', false, context);
console.log(result.value);   // true
console.log(result.reason);  // 'TARGETING_MATCH'
console.log(result.variant); // 'enabled'

String Flags

typescript
// Get the string value
const variant = await flags.getStringValue('checkout-flow', 'control', context);

// Use for A/B testing
if (variant === 'treatment-a') {
  showNewCheckout();
} else {
  showOriginalCheckout();
}

Number Flags

typescript
// Get numeric configuration
const rateLimit = await flags.getNumberValue('api-rate-limit', 100, context);
const discount = await flags.getNumberValue('promo-discount', 0, context);

JSON Flags

typescript
// Get complex configuration
const config = await flags.getJsonValue('feature-config', {
  maxItems: 10,
  showBanner: false,
}, context);

console.log(config.maxItems);   // 50
console.log(config.showBanner); // true

Evaluation Context

The evaluation context determines which flag value a user receives. Use it for targeting rules and percentage rollouts.

targetingKey

The targetingKey is a unique identifier for the user or device. It's used for:

  • Percentage rollouts (consistent bucketing)
  • User-specific targeting rules
  • Analytics and debugging
typescript
const context = {
  targetingKey: 'user-123', // Required for percentage rollouts
};

Custom Attributes

Add any attributes for targeting rules:

typescript
const context = {
  targetingKey: 'user-123',
  // Custom attributes for targeting
  plan: 'premium',
  country: 'US',
  email: 'user@example.com',
  beta_tester: true,
  signup_date: '2024-01-15',
};

Attribute Naming

Use lowercase snake_case for attribute names to match Kitbase conventions.

Resolution Details

All get*Details methods return full resolution information:

typescript
interface ResolutionDetails<T> {
  value: T;                           // The resolved flag value
  variant?: string;                   // Variant identifier (e.g., "control", "treatment-a")
  reason: ResolutionReason;           // Why this value was returned
  errorCode?: ErrorCode;              // Error code if evaluation failed
  errorMessage?: string;              // Human-readable error message
  flagMetadata?: Record<string, unknown>; // Additional flag metadata
}

Resolution Reasons

ReasonDescription
STATICFlag has a static value (no rules)
DEFAULTDefault value returned (flag disabled or not found)
TARGETING_MATCHValue determined by targeting rules
SPLITValue determined by percentage rollout
CACHEDValue returned from cache
DISABLEDFlag is disabled
ERROREvaluation encountered an error

Flag Snapshot

Get all evaluated flags at once for better performance:

typescript
const snapshot = await flags.getSnapshot({
  context: {
    targetingKey: 'user-123',
    plan: 'premium',
  },
});

// Access individual flags from snapshot
for (const flag of snapshot.flags) {
  console.log(`${flag.flagKey}: ${flag.value} (${flag.reason})`);
}
dart
final snapshot = await flags.getSnapshot(
  options: EvaluateOptions(
    context: EvaluationContext(
      targetingKey: 'user-123',
      attributes: {'plan': 'premium'},
    ),
  ),
);

for (final flag in snapshot.flags) {
  print('${flag.flagKey}: ${flag.value} (${flag.reason})');
}
php
$snapshot = $flags->getSnapshot(new EvaluationContext(
    targetingKey: 'user-123',
    attributes: ['plan' => 'premium'],
));

foreach ($snapshot->flags as $flag) {
    echo "{$flag->flagKey}: {$flag->value} ({$flag->reason->value})\n";
}

OpenFeature Integration

Kitbase provides OpenFeature providers for standardized feature flag evaluation.

Server-Side (Node.js)

typescript
import { OpenFeature } from '@openfeature/server-sdk';
import { KitbaseProvider } from '@kitbase/flags/server';

// Register the provider
await OpenFeature.setProviderAndWait(new KitbaseProvider({
  token: '<YOUR_API_KEY>',
  cache: true,      // Enable caching (default: true)
  cacheTtl: 60000,  // Cache TTL in ms (default: 1 minute)
}));

// Get a client
const client = OpenFeature.getClient();

// Evaluate flags using OpenFeature API
const isEnabled = await client.getBooleanValue('dark-mode', false, {
  targetingKey: 'user-123',
  plan: 'premium',
});

Client-Side (Web)

typescript
import { OpenFeature } from '@openfeature/web-sdk';
import { KitbaseProvider } from '@kitbase/flags/web';

// Register the provider with initial context
await OpenFeature.setProviderAndWait(
  new KitbaseProvider({
    token: '<YOUR_API_KEY>',
    prefetchOnInit: true, // Fetch all flags on init (default: true)
    cacheTtl: 300000,     // Cache TTL in ms (default: 5 minutes)
  }),
  {
    targetingKey: 'user-123',
    plan: 'premium',
  }
);

// Get a client - evaluations use cached values
const client = OpenFeature.getClient();
const isEnabled = client.getBooleanValue('dark-mode', false);

// Refresh flags when needed
await OpenFeature.getProvider().refresh();

Dart (Server-Side)

dart
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';
import 'package:kitbase/flags/openfeature.dart';

// Register the provider
final api = OpenFeatureAPI();
api.setProvider(KitbaseProvider(
  options: KitbaseProviderOptions(
    token: '<YOUR_API_KEY>',
    cache: true,
    cacheTtl: 60000,
  ),
));

// Create a client
final client = FeatureClient(
  metadata: ClientMetadata(name: 'my-app'),
  hookManager: HookManager(),
  defaultContext: EvaluationContext(attributes: {}),
);

// Evaluate flags
final isEnabled = await client.getBooleanFlag(
  'dark-mode',
  defaultValue: false,
  context: EvaluationContext(attributes: {
    'targetingKey': 'user-123',
    'plan': 'premium',
  }),
);

Error Handling

See Error Handling for comprehensive error handling patterns.

typescript
import {
  FlagsClient,
  FlagsError,
  AuthenticationError,
  FlagNotFoundError,
  TypeMismatchError,
  ValidationError,
  TimeoutError,
  ApiError,
} from '@kitbase/flags';

try {
  const value = await flags.getBooleanValue('my-flag', false, context);
} catch (error) {
  if (error instanceof AuthenticationError) {
    // Invalid API key
  } else if (error instanceof FlagNotFoundError) {
    console.log(`Flag ${error.flagKey} not found`);
  } else if (error instanceof TypeMismatchError) {
    console.log(`Expected ${error.expectedType}, got ${error.actualType}`);
  } else if (error instanceof ValidationError) {
    console.log(`Validation error: ${error.field}`);
  } else if (error instanceof TimeoutError) {
    // Request timed out
  } else if (error instanceof ApiError) {
    console.log(`API error: ${error.statusCode}`);
  }
}
dart
import 'package:kitbase/flags.dart';

try {
  final value = await flags.getBooleanValue('my-flag', false, context: context);
} on FlagsAuthenticationException {
  // Invalid API key
} on FlagNotFoundException catch (e) {
  print('Flag ${e.flagKey} not found');
} on TypeMismatchException catch (e) {
  print('Expected ${e.expectedType}, got ${e.actualType}');
} on FlagsValidationException catch (e) {
  print('Validation error: ${e.field}');
} on FlagsTimeoutException {
  // Request timed out
} on FlagsApiException catch (e) {
  print('API error: ${e.statusCode}');
}
php
use Kitbase\Flags\AuthenticationException;
use Kitbase\Flags\FlagNotFoundException;
use Kitbase\Flags\TypeMismatchException;
use Kitbase\Flags\ValidationException;
use Kitbase\Flags\TimeoutException;
use Kitbase\Flags\ApiException;

try {
    $value = $flags->getBooleanValue('my-flag', false, $context);
} catch (AuthenticationException $e) {
    // Invalid API key
} catch (FlagNotFoundException $e) {
    echo "Flag {$e->flagKey} not found";
} catch (TypeMismatchException $e) {
    echo "Expected {$e->expectedType}, got {$e->actualType}";
} catch (ValidationException $e) {
    echo "Validation error: {$e->field}";
} catch (TimeoutException $e) {
    // Request timed out
} catch (ApiException $e) {
    echo "API error: {$e->statusCode}";
}

Best Practices

1. Always Provide Default Values

Default values ensure your app works even when flags can't be evaluated:

typescript
// Good - provides a sensible default
const limit = await flags.getNumberValue('rate-limit', 100, context);

// The default is used when:
// - Flag doesn't exist
// - Flag is disabled
// - Network error occurs
// - Type mismatch

2. Use Meaningful Targeting Keys

Use consistent, unique identifiers for targeting:

typescript
// Good - unique user ID
{ targetingKey: 'user-123' }
{ targetingKey: 'org-456' }

// Bad - not unique or consistent
{ targetingKey: 'john@example.com' }  // Email can change
{ targetingKey: 'session-xyz' }       // Different per session

3. Cache Flag Values

For client-side applications, cache flags to reduce latency:

typescript
// Use the OpenFeature web provider with prefetching
const provider = new KitbaseProvider({
  token: '<YOUR_API_KEY>',
  prefetchOnInit: true,  // Fetch all flags on init
  cacheTtl: 300000,      // Cache for 5 minutes
});

4. Handle Errors Gracefully

Don't let flag evaluation failures break your app:

typescript
async function getFeatureValue<T>(
  flagKey: string,
  defaultValue: T,
  context: EvaluationContext
): Promise<T> {
  try {
    return await flags.getBooleanValue(flagKey, defaultValue, context);
  } catch (error) {
    console.error(`Flag evaluation failed for ${flagKey}:`, error);
    return defaultValue; // Return default on error
  }
}

5. Use Flag Snapshots for Multiple Flags

When evaluating multiple flags, use snapshots for better performance:

typescript
// Bad - multiple network requests
const flag1 = await flags.getBooleanValue('flag-1', false, context);
const flag2 = await flags.getBooleanValue('flag-2', false, context);
const flag3 = await flags.getStringValue('flag-3', 'default', context);

// Good - single network request
const snapshot = await flags.getSnapshot({ context });
const flag1 = snapshot.flags.find(f => f.flagKey === 'flag-1')?.value ?? false;
const flag2 = snapshot.flags.find(f => f.flagKey === 'flag-2')?.value ?? false;
const flag3 = snapshot.flags.find(f => f.flagKey === 'flag-3')?.value ?? 'default';

Use Cases

Feature Rollout

Gradually roll out a feature to users:

typescript
const isEnabled = await flags.getBooleanValue('new-checkout', false, {
  targetingKey: user.id,
});

if (isEnabled) {
  return <NewCheckout />;
} else {
  return <OldCheckout />;
}

A/B Testing

Run experiments with multiple variants:

typescript
const variant = await flags.getStringValue('pricing-page', 'control', {
  targetingKey: user.id,
});

switch (variant) {
  case 'treatment-a':
    return <PricingPageA />;
  case 'treatment-b':
    return <PricingPageB />;
  default:
    return <PricingPageControl />;
}

Configuration Management

Use flags for runtime configuration:

typescript
const config = await flags.getJsonValue('api-config', {
  timeout: 30000,
  retries: 3,
  batchSize: 100,
}, { targetingKey: 'api-service' });

const client = new ApiClient({
  timeout: config.timeout,
  retries: config.retries,
});

Beta Features

Enable features for beta testers:

typescript
const isEnabled = await flags.getBooleanValue('beta-feature', false, {
  targetingKey: user.id,
  beta_tester: user.isBetaTester,
});

Kill Switches

Instantly disable features in production:

typescript
const isDisabled = await flags.getBooleanValue('disable-payments', false);

if (isDisabled) {
  return <PaymentsDisabledPage />;
}

Local Evaluation

For high-performance scenarios, the FlagsClient supports local evaluation mode which evaluates flags locally without making network requests for each evaluation. The client fetches the flag configuration once and keeps it updated via polling.

Benefits

  • Zero latency - Evaluations happen locally, no network calls
  • Reduced API calls - Configuration fetched once, reused for all evaluations
  • Offline support - Continue evaluating flags even when offline
  • Automatic updates - Stay in sync via configurable polling
  • SSR compatible - Bootstrap with initial config for server-side rendering
  • Simple API - Automatic initialization, no manual setup required

Quick Start

Enable local evaluation by setting enableLocalEvaluation: true:

typescript
import { FlagsClient } from '@kitbase/flags';

const flags = new FlagsClient({
  token: '<YOUR_API_KEY>',
  enableLocalEvaluation: true,
  environmentRefreshIntervalSeconds: 60, // Poll every 60 seconds
});

// Initialization happens automatically on first evaluation
// (or call await flags.initialize() to initialize early)

// Evaluates locally (no network call per evaluation)
const isEnabled = await flags.getBooleanValue('dark-mode', false, {
  targetingKey: 'user-123',
  plan: 'premium',
});

if (isEnabled) {
  enableDarkMode();
}

// Clean up when done
flags.close();

Configuration Options

OptionTypeDefaultDescription
tokenstringRequiredYour Kitbase API key
enableLocalEvaluationbooleanfalseEnable local evaluation mode
environmentRefreshIntervalSecondsnumber60Polling interval in seconds (0 to disable)
initialConfigurationFlagConfiguration-Bootstrap config for SSR/offline
onConfigurationChangefunction-Callback when config updates
onErrorfunction-Callback when errors occur
typescript
const flags = new FlagsClient({
  token: '<YOUR_API_KEY>',
  enableLocalEvaluation: true,
  environmentRefreshIntervalSeconds: 30, // Poll every 30 seconds
  onConfigurationChange: (config) => {
    console.log('Flags updated at:', config.generatedAt);
  },
  onError: (error) => {
    console.error('Flag sync error:', error);
  },
});

Update Modes

Choose how the client receives configuration updates:

Polling (Default)

The client periodically fetches the latest configuration:

typescript
const flags = new FlagsClient({
  token: '<YOUR_API_KEY>',
  enableLocalEvaluation: true,
  environmentRefreshIntervalSeconds: 60, // Every 60 seconds (default)
});

Uses ETag-based cache validation to minimize bandwidth - the server returns 304 Not Modified if nothing changed.

Polling Intervals

For most use cases, polling with a 30-60 second interval provides a good balance between freshness and performance. Use shorter intervals (10-30 seconds) for time-sensitive features, and longer intervals (2-5 minutes) for less critical flags.

Event Handling

Listen to client lifecycle events:

typescript
const flags = new FlagsClient({
  token: '<YOUR_API_KEY>',
  enableLocalEvaluation: true,
});

// Add event listener
const unsubscribe = flags.on((event) => {
  switch (event.type) {
    case 'ready':
      console.log('Flags loaded:', event.config.flags.length);
      break;
    case 'configurationChanged':
      console.log('Flags updated at:', event.config.generatedAt);
      // Optionally re-render UI
      break;
    case 'error':
      console.error('Sync error:', event.error);
      break;
  }
});

// Initialize early (optional - happens automatically on first evaluation)
await flags.initialize();

// Remove listener when done
unsubscribe();
EventDescription
readyInitial configuration loaded successfully
configurationChangedConfiguration updated (new flags/rules)
errorError during configuration fetch

Offline & SSR Support

Bootstrap the client with pre-fetched configuration for offline-first or server-side rendering:

typescript
// Server-side: Fetch config during SSR
const response = await fetch('https://api.kitbase.dev/v1/feature-flags/config', {
  headers: { 'X-API-Key': '<YOUR_API_KEY>' },
});
const initialConfiguration = await response.json();

// Client-side: Initialize with prefetched config
const flags = new FlagsClient({
  token: '<YOUR_API_KEY>',
  enableLocalEvaluation: true,
  initialConfiguration, // Use immediately, no blocking fetch
});

// Flags are ready instantly when initialConfiguration is provided
const isEnabled = await flags.getBooleanValue('feature', false, context);

For offline scenarios, the client continues evaluating with the last known configuration until connectivity is restored.

API Reference

When enableLocalEvaluation is true, the following additional methods are available:

typescript
// Check readiness (always true for remote mode)
flags.isReady(): boolean;

// Configuration access (local evaluation mode only)
flags.getConfiguration(): FlagConfiguration | null;
flags.hasFlag(flagKey): boolean;
flags.getFlagKeys(): string[];

// Lifecycle (local evaluation mode only)
flags.initialize(): Promise<void>;
flags.refresh(): Promise<void>;
flags.close(): void;

All flag evaluation methods (getBooleanValue, getStringValue, etc.) work the same way in both modes - just with local evaluation providing faster response times.

Comparison: Remote vs Local Evaluation

FeatureRemote EvaluationLocal Evaluation
ConfigurationDefaultenableLocalEvaluation: true
Evaluation locationServer-sideClient-side (local)
Network per evaluationYesNo
Latency~50-200ms<1ms
Offline supportNoYes (with initialConfiguration)
Automatic updatesNoYes (polling)
InitializationNot requiredAutomatic on first evaluation
Best forServer apps, low-frequencyHigh-frequency, latency-sensitive

Released under the MIT License.