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
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();
}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();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:
| Type | Description | Example |
|---|---|---|
boolean | On/off toggles | true, false |
string | Text values, variants | "control", "treatment-a" |
number | Numeric values | 42, 3.14 |
json | Complex objects | {"limit": 100, "tier": "pro"} |
Boolean Flags
// 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
// 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
// Get numeric configuration
const rateLimit = await flags.getNumberValue('api-rate-limit', 100, context);
const discount = await flags.getNumberValue('promo-discount', 0, context);JSON Flags
// Get complex configuration
const config = await flags.getJsonValue('feature-config', {
maxItems: 10,
showBanner: false,
}, context);
console.log(config.maxItems); // 50
console.log(config.showBanner); // trueEvaluation 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
const context = {
targetingKey: 'user-123', // Required for percentage rollouts
};Custom Attributes
Add any attributes for targeting rules:
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:
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
| Reason | Description |
|---|---|
STATIC | Flag has a static value (no rules) |
DEFAULT | Default value returned (flag disabled or not found) |
TARGETING_MATCH | Value determined by targeting rules |
SPLIT | Value determined by percentage rollout |
CACHED | Value returned from cache |
DISABLED | Flag is disabled |
ERROR | Evaluation encountered an error |
Flag Snapshot
Get all evaluated flags at once for better performance:
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})`);
}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})');
}$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)
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)
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)
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.
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}`);
}
}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}');
}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:
// 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 mismatch2. Use Meaningful Targeting Keys
Use consistent, unique identifiers for targeting:
// 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 session3. Cache Flag Values
For client-side applications, cache flags to reduce latency:
// 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:
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:
// 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:
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:
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:
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:
const isEnabled = await flags.getBooleanValue('beta-feature', false, {
targetingKey: user.id,
beta_tester: user.isBetaTester,
});Kill Switches
Instantly disable features in production:
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:
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
| Option | Type | Default | Description |
|---|---|---|---|
token | string | Required | Your Kitbase API key |
enableLocalEvaluation | boolean | false | Enable local evaluation mode |
environmentRefreshIntervalSeconds | number | 60 | Polling interval in seconds (0 to disable) |
initialConfiguration | FlagConfiguration | - | Bootstrap config for SSR/offline |
onConfigurationChange | function | - | Callback when config updates |
onError | function | - | Callback when errors occur |
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:
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:
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();| Event | Description |
|---|---|
ready | Initial configuration loaded successfully |
configurationChanged | Configuration updated (new flags/rules) |
error | Error during configuration fetch |
Offline & SSR Support
Bootstrap the client with pre-fetched configuration for offline-first or server-side rendering:
// 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:
// 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
| Feature | Remote Evaluation | Local Evaluation |
|---|---|---|
| Configuration | Default | enableLocalEvaluation: true |
| Evaluation location | Server-side | Client-side (local) |
| Network per evaluation | Yes | No |
| Latency | ~50-200ms | <1ms |
| Offline support | No | Yes (with initialConfiguration) |
| Automatic updates | No | Yes (polling) |
| Initialization | Not required | Automatic on first evaluation |
| Best for | Server apps, low-frequency | High-frequency, latency-sensitive |