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
Packages
| Package | Description | Install |
|---|---|---|
@kitbase/flags | Core SDK (vanilla JS) — also available via CDN | npm install @kitbase/flags |
@kitbase/flags-react | React hooks & provider | npm install @kitbase/flags-react |
@kitbase/flags-angular | Angular signals & services | npm install @kitbase/flags-angular |
Quick Start
import { FlagsClient } from '@kitbase/flags';
const flags = new FlagsClient({
sdkKey: '<YOUR_SDK_KEY>',
defaultValues: {
'dark-mode': false,
},
});
// Works immediately — anonymous ID is used for rollouts
const isEnabled = await flags.getBooleanValue('dark-mode');
if (isEnabled) {
enableDarkMode();
}
// After login, identify the user for user-specific targeting
flags.identify('user-123', { plan: 'premium' });import { FlagsProvider, useBooleanFlag } from '@kitbase/flags-react';
function App() {
return (
<FlagsProvider config={{ sdkKey: '<YOUR_SDK_KEY>' }}>
<Feature />
</FlagsProvider>
);
}
function Feature() {
const { data: isDarkMode, isLoading } = useBooleanFlag('dark-mode', {
context: { targetingKey: 'user-123' },
});
if (isLoading) return <Spinner />;
return <App theme={isDarkMode ? 'dark' : 'light'} />;
}// app.config.ts
import { provideFlags } from '@kitbase/flags-angular';
export const appConfig: ApplicationConfig = {
providers: [
provideFlags({ sdkKey: '<YOUR_SDK_KEY>' }),
],
};
// feature.component.ts
import { Component, inject } from '@angular/core';
import { FlagsService } from '@kitbase/flags-angular';
@Component({
selector: 'app-feature',
template: `
@if (darkMode().isLoading) {
<p>Loading...</p>
} @else {
<p>Dark mode: {{ darkMode().value }}</p>
}
`,
})
export class FeatureComponent {
private flags = inject(FlagsService);
darkMode = this.flags.getBooleanFlag('dark-mode');
}import 'package:kitbase/flags.dart';
final flags = KitbaseFlags(token: '<YOUR_SDK_KEY>');
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_SDK_KEY>',
));
$isEnabled = $flags->getBooleanValue('dark-mode', false, new EvaluationContext(
targetingKey: 'user-123',
attributes: ['plan' => 'premium'],
));
if ($isEnabled) {
enableDarkMode();
}Script Tag (CDN)
No bundler required. Works with any page — PHP, WordPress, static HTML, etc.
<script src="https://unpkg.com/@kitbase/flags/dist/index.global.js"></script><script src="https://cdn.jsdelivr.net/npm/@kitbase/flags/dist/index.global.js"></script>The script exposes a KitbaseFlags global containing FlagsClient, KitbaseProvider, and OpenFeature — the OpenFeature web SDK is bundled in.
<script src="https://unpkg.com/@kitbase/flags/dist/index.global.js"></script>
<script>
var flags = new KitbaseFlags.FlagsClient({
sdkKey: '<YOUR_SDK_KEY>',
defaultValues: { 'dark-mode': false },
});
flags.identify('user-123', { plan: 'premium' });
flags.getBooleanValue('dark-mode').then(function (enabled) {
if (enabled) enableDarkMode();
});
</script>Or use it as an OpenFeature provider:
<script src="https://unpkg.com/@kitbase/flags/dist/index.global.js"></script>
<script>
var { FlagsClient, KitbaseProvider, OpenFeature } = KitbaseFlags;
var client = new FlagsClient({ sdkKey: '<YOUR_SDK_KEY>' });
OpenFeature.setContext({ targetingKey: 'user-123' });
OpenFeature.setProviderAndWait(new KitbaseProvider(client)).then(function () {
var ofClient = OpenFeature.getClient();
var enabled = ofClient.getBooleanValue('dark-mode', false);
});
</script>Pin a version in production
Replace the URL with a specific version to avoid surprise upgrades:
<script src="https://unpkg.com/@kitbase/flags@0.2.0/dist/index.global.js"></script>Configuration
import { FlagsClient } from '@kitbase/flags';
const flags = new FlagsClient({
// Required
sdkKey: '<YOUR_SDK_KEY>',
// Optional: API base URL (for self-hosting)
baseUrl: 'https://api.kitbase.dev',
// Optional: Global default values
// Used as fallback when a flag is disabled, not found, or returns an error
defaultValues: {
'dark-mode': false,
'api-url': 'https://api.default.com',
'max-items': 10,
},
// Optional: Local evaluation settings
localEvaluation: {
enabled: false, // Enable local evaluation mode
refreshIntervalSeconds: 60, // Polling interval (0 to disable)
initialConfiguration: undefined, // Bootstrap config for SSR/offline
},
// Optional: Remote evaluation cache settings
remoteEvaluationCache: {
ttl: 60000, // Cache TTL in ms (default: 1 minute)
persistent: true, // Use localStorage (default: true in browser)
},
// Optional: Callbacks (local evaluation only)
onConfigurationChange: (config) => {
console.log('Flags updated at:', config.generatedAt);
},
onError: (error) => {
console.error('Flag sync error:', error);
},
});Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
sdkKey | string | required | Your Kitbase SDK key |
baseUrl | string | 'https://api.kitbase.dev' | API base URL (for self-hosting) |
defaultValues | Record<string, unknown> | {} | Global fallback values for disabled/missing flags |
localEvaluation | LocalEvaluationConfig | — | Local evaluation settings (see below) |
remoteEvaluationCache | RemoteEvaluationCacheConfig | — | Cache settings for remote evaluation |
onConfigurationChange | (config) => void | — | Callback when config updates (local eval only) |
onError | (error) => void | — | Callback on errors (local eval only) |
Local Evaluation Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable local evaluation mode |
refreshIntervalSeconds | number | 60 | Polling interval in seconds (0 to disable) |
initialConfiguration | FlagConfiguration | — | Bootstrap config for SSR/offline |
Remote Evaluation Cache Options
| Option | Type | Default | Description |
|---|---|---|---|
ttl | number | 60000 | Cache TTL in milliseconds |
persistent | boolean | true in browser | Use localStorage for persistent caching |
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
const isEnabled = await flags.getBooleanValue('dark-mode');
// With evaluation context
const isEnabled = await flags.getBooleanValue('dark-mode', {
targetingKey: 'user-123',
plan: 'premium',
});String Flags
const variant = await flags.getStringValue('checkout-flow', context);
// Use for A/B testing
if (variant === 'treatment-a') {
showNewCheckout();
} else {
showOriginalCheckout();
}Number Flags
const rateLimit = await flags.getNumberValue('api-rate-limit', context);
const discount = await flags.getNumberValue('promo-discount', context);JSON Flags
const config = await flags.getJsonValue('feature-config', context);
console.log(config.maxItems); // 50
console.log(config.showBanner); // trueFull Evaluation Details
Use evaluateFlag() to get full resolution details including reason, variant, and metadata:
const result = await flags.evaluateFlag('dark-mode', {
context: { targetingKey: 'user-123' },
});
console.log(result.enabled); // true
console.log(result.value); // true
console.log(result.reason); // 'TARGETING_MATCH'
console.log(result.variant); // 'enabled'
console.log(result.valueType); // 'boolean'Default Values
Default values are configured globally via defaultValues in the client config. They are used as fallback when:
- A flag is disabled
- A flag is not found
- The backend returns an error
const flags = new FlagsClient({
sdkKey: '<YOUR_SDK_KEY>',
defaultValues: {
'dark-mode': false,
'max-items': 10,
'checkout-flow': 'control',
},
});
// If 'dark-mode' is disabled, returns false (from defaultValues)
const isEnabled = await flags.getBooleanValue('dark-mode');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
The SDK automatically generates and persists an anonymous targetingKey, so you don't need to set it manually. Use identify() after login to switch to a real user ID:
// Anonymous — auto-generated targetingKey is used
const isEnabled = await flags.getBooleanValue('feature');
// After login — sets targetingKey to the real user ID
flags.identify('user-123');
// Or pass it explicitly per-call if needed
const value = await flags.getBooleanValue('feature', {
targetingKey: 'user-456',
});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.
User Identity
The SDK automatically generates an anonymous ID on first use and persists it to localStorage. This means percentage rollouts and targeting rules work out of the box — even before the user logs in.
After login, call identify() to link the user to a real ID. The anonymous ID is preserved for when the user logs out.
Anonymous ID
On init, the SDK generates a UUID and stores it as kitbase_anonymous_id in localStorage — the same key used by @kitbase/analytics, so both SDKs share the same anonymous identity. This ID is used as the targetingKey for all evaluations until identify() is called.
const flags = new FlagsClient({ sdkKey: '<YOUR_SDK_KEY>' });
// Anonymous ID is already set — rollouts work immediately
const isEnabled = await flags.getBooleanValue('new-feature');
// Access it if needed
const anonId = flags.getAnonymousId();In server-side or Node.js environments where localStorage is unavailable, the SDK generates a per-instance ID (not persisted across restarts).
identify(targetingKey, traits?)
Set the current user. The targetingKey and any traits are merged into every flag evaluation. Call this after login.
flags.identify('user-123', {
plan: 'premium',
country: 'US',
beta_tester: true,
});
// All evaluations now use 'user-123' as the targetingKey
const isEnabled = await flags.getBooleanValue('dark-mode');
// Per-call context overrides identity values
const result = await flags.getBooleanValue('feature', {
country: 'DE', // overrides 'US' from identify()
});resetIdentity()
Clear the current identity. Call this on logout. Reverts to the anonymous ID.
flags.resetIdentity();getTargetingKey()
Get the current identified targeting key, or null if not identified (anonymous ID is still used for evaluation but not returned here).
const key = flags.getTargetingKey(); // 'user-123' or nullContext Priority
The targetingKey used for evaluation is resolved in this order:
- Per-call context —
flags.getBooleanValue('flag', { targetingKey: 'explicit' }) - Identity — set via
flags.identify('user-123') - Anonymous ID — auto-generated, persisted to localStorage
Resolution Details
The evaluateFlag() method returns full resolution information:
interface EvaluatedFlag {
flagKey: string; // The flag key
enabled: boolean; // Whether the flag is enabled
valueType: FlagValueType; // 'boolean' | 'string' | 'number' | 'json'
value: unknown; // The resolved value (null if disabled)
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 (no rules matched) |
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";
}React SDK
The @kitbase/flags-react package provides React hooks and a context provider for feature flag evaluation.
Installation
npm install @kitbase/flags-reactpnpm add @kitbase/flags-reactyarn add @kitbase/flags-reactProvider Setup
Wrap your app with FlagsProvider:
import { FlagsProvider } from '@kitbase/flags-react';
function App() {
return (
<FlagsProvider config={{
sdkKey: '<YOUR_SDK_KEY>',
defaultValues: { 'dark-mode': false },
}}>
<YourApp />
</FlagsProvider>
);
}For local evaluation:
<FlagsProvider config={{
sdkKey: '<YOUR_SDK_KEY>',
localEvaluation: { enabled: true, refreshIntervalSeconds: 60 },
}}>
<YourApp />
</FlagsProvider>Hooks
useBooleanFlag(flagKey, options?)
function Feature() {
const { data: isDarkMode, isLoading, error } = useBooleanFlag('dark-mode', {
context: { targetingKey: userId, plan: 'premium' },
});
if (isLoading) return <Spinner />;
return <App theme={isDarkMode ? 'dark' : 'light'} />;
}useStringFlag(flagKey, options?)
function Checkout() {
const { data: variant } = useStringFlag('checkout-variant', {
context: { targetingKey: userId },
});
return <Checkout variant={variant} />;
}useNumberFlag(flagKey, options?)
function Cart() {
const { data: maxItems } = useNumberFlag('max-cart-items', {
context: { targetingKey: userId },
});
return <Cart maxItems={maxItems} />;
}useJsonFlag<T>(flagKey, options?)
interface FeatureConfig {
enabled: boolean;
theme: string;
maxItems: number;
}
function Feature() {
const { data: config } = useJsonFlag<FeatureConfig>(
'feature-config',
{ context: { targetingKey: userId } },
);
return <Feature config={config} />;
}useFlagSnapshot(options?)
Get a snapshot of all flags at once:
function FlagsDebugger() {
const { data: snapshot, isLoading, error } = useFlagSnapshot({
context: { targetingKey: userId },
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<ul>
{snapshot?.flags.map(flag => (
<li key={flag.flagKey}>
{flag.flagKey}: {String(flag.value)}
</li>
))}
</ul>
);
}Hook Options
| Option | Type | Default | Description |
|---|---|---|---|
context | EvaluationContext | — | Evaluation context for targeting |
refetchOnContextChange | boolean | true | Re-evaluate when context changes |
Hook Return Type
All flag hooks return UseFlagResult<T>:
| Field | Type | Description |
|---|---|---|
data | T | undefined | The flag value |
isLoading | boolean | Whether the flag is being evaluated |
error | Error | null | Any error during evaluation |
refetch | () => Promise<void> | Manually re-evaluate the flag |
Hooks automatically re-evaluate when the flag's value changes (via polling or configuration updates).
Angular SDK
The @kitbase/flags-angular package provides Angular signals, observables, and DI-based services.
Installation
npm install @kitbase/flags-angularpnpm add @kitbase/flags-angularyarn add @kitbase/flags-angularProvider Setup
Register the provider in your app config:
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideFlags } from '@kitbase/flags-angular';
export const appConfig: ApplicationConfig = {
providers: [
provideFlags({
sdkKey: '<YOUR_SDK_KEY>',
localEvaluation: {
enabled: true,
refreshIntervalSeconds: 60,
},
}),
],
};Signal-based API
Flags as reactive Angular Signals that automatically update when configuration changes:
import { Component, inject } from '@angular/core';
import { FlagsService } from '@kitbase/flags-angular';
@Component({
selector: 'app-feature',
template: `
@if (darkMode().isLoading) {
<p>Loading...</p>
} @else {
<p>Dark mode is {{ darkMode().value ? 'enabled' : 'disabled' }}</p>
}
`,
})
export class FeatureComponent {
private flags = inject(FlagsService);
darkMode = this.flags.getBooleanFlag('dark-mode');
// With context
premiumFeature = this.flags.getBooleanFlag('premium-feature', {
context: { targetingKey: 'user-123', plan: 'premium' },
});
}Signal methods: getBooleanFlag(), getStringFlag(), getNumberFlag(), getJsonFlag()
Each returns Signal<FlagSignalResult<T>>:
| Field | Type | Description |
|---|---|---|
value | T | undefined | The flag value (undefined while loading) |
isLoading | boolean | Whether the flag is currently loading |
error | Error | null | Error if flag evaluation failed |
Observable-based API
Flags as RxJS Observables that re-emit when the flag value changes:
@Component({
selector: 'app-feature',
template: `
<p>Dark mode: {{ darkMode$ | async }}</p>
`,
})
export class FeatureComponent {
private flags = inject(FlagsService);
darkMode$ = this.flags.getBooleanFlag$('dark-mode');
variant$ = this.flags.getStringFlag$('checkout-variant', context);
limit$ = this.flags.getNumberFlag$('rate-limit');
config$ = this.flags.getJsonFlag$('feature-config');
}One-shot Promise API
For cases where you don't need reactivity:
const isEnabled = await this.flags.getBooleanValue('dark-mode');
const variant = await this.flags.getStringValue('checkout-variant', context);
const limit = await this.flags.getNumberValue('rate-limit');
const config = await this.flags.getJsonValue('feature-config');Service Observables
The FlagsService exposes lifecycle observables:
| Observable | Description |
|---|---|
isReady$ | Emits true when the client is ready |
configurationChanged$ | Emits when flag configuration updates from backend |
flagsChanged$ | Emits a map of changed flag keys to their new values |
Flag Change Listeners
Subscribe to flag value changes (works in all modes):
const { unsubscribe } = flags.onFlagChange((changedFlags) => {
console.log('Flags changed:', changedFlags);
// changedFlags = { 'dark-mode': true, 'max-items': 50 }
if ('dark-mode' in changedFlags) {
updateTheme(changedFlags['dark-mode']);
}
});
// Later, to stop listening:
unsubscribe();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({
sdkKey: '<YOUR_SDK_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)
The web provider takes the full FlagsConfig and prefetches all flags on initialization:
import { OpenFeature } from '@openfeature/web-sdk';
import { KitbaseProvider } from '@kitbase/flags/web';
// Register the provider with initial context
await OpenFeature.setProviderAndWait(
new KitbaseProvider({
sdkKey: '<YOUR_SDK_KEY>',
}),
{
targetingKey: 'user-123',
plan: 'premium',
},
);
// Get a client - evaluations use cached values (synchronous)
const client = OpenFeature.getClient();
const isEnabled = client.getBooleanValue('dark-mode', false);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_SDK_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', context);
} catch (error) {
if (error instanceof AuthenticationError) {
// Invalid SDK 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}";
}Error Types
| Error | Description | Properties |
|---|---|---|
FlagsError | Base class for all flag errors | message |
AuthenticationError | Invalid SDK key | — |
ApiError | API returned an error | statusCode, response |
ValidationError | Invalid input | field |
TimeoutError | Request timed out | — |
FlagNotFoundError | Flag doesn't exist | flagKey |
TypeMismatchError | Wrong type requested | flagKey, expectedType, actualType |
InvalidContextError | Invalid evaluation context | — |
ParseError | Failed to parse flag configuration | — |
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 via the localEvaluation config:
import { FlagsClient } from '@kitbase/flags';
const flags = new FlagsClient({
sdkKey: '<YOUR_SDK_KEY>',
localEvaluation: {
enabled: true,
refreshIntervalSeconds: 60, // Poll every 60 seconds
},
});
// Initialization happens automatically on first evaluation
// (or call await flags.waitUntilReady() to initialize early)
// Evaluates locally (no network call per evaluation)
const isEnabled = await flags.getBooleanValue('dark-mode', {
targetingKey: 'user-123',
plan: 'premium',
});
if (isEnabled) {
enableDarkMode();
}
// Clean up when done
flags.close();Update Mode: Polling
The client periodically fetches the latest configuration:
const flags = new FlagsClient({
sdkKey: '<YOUR_SDK_KEY>',
localEvaluation: {
enabled: true,
refreshIntervalSeconds: 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({
sdkKey: '<YOUR_SDK_KEY>',
localEvaluation: { enabled: 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;
}
});
// Wait for ready (optional - happens automatically on first evaluation)
await flags.waitUntilReady();
// 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/sdk/v1/feature-flags/config', {
headers: { 'x-sdk-key': '<YOUR_SDK_KEY>' },
});
const initialConfiguration = await response.json();
// Client-side: Initialize with prefetched config
const flags = new FlagsClient({
sdkKey: '<YOUR_SDK_KEY>',
localEvaluation: {
enabled: true,
initialConfiguration, // Use immediately, no blocking fetch
},
});
// Flags are ready instantly when initialConfiguration is provided
const isEnabled = await flags.getBooleanValue('feature', context);For offline scenarios, the client continues evaluating with the last known configuration until connectivity is restored.
API Reference
When localEvaluation.enabled is true, the following additional methods are available:
// Check readiness (always true for remote mode)
flags.isReady(): boolean;
// Wait for initialization
flags.waitUntilReady(): Promise<void>;
// Configuration access (local evaluation mode only)
flags.getConfiguration(): FlagConfiguration | null;
flags.hasFlag(flagKey): boolean;
flags.getFlagKeys(): string[];
// Lifecycle
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 | localEvaluation: { enabled: 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 |
Best Practices
1. Use Default Values
Default values ensure your app works even when flags can't be evaluated:
const flags = new FlagsClient({
sdkKey: '<YOUR_SDK_KEY>',
defaultValues: {
'rate-limit': 100,
'checkout-flow': 'control',
'dark-mode': false,
},
});
// The default is used when:
// - Flag doesn't exist
// - Flag is disabled
// - Network error occurs (remote evaluation)
const limit = await flags.getNumberValue('rate-limit');2. 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. Use Flag Snapshots for Multiple Flags
When evaluating multiple flags, use snapshots for better performance:
// Bad - multiple network requests (remote evaluation)
const flag1 = await flags.getBooleanValue('flag-1', context);
const flag2 = await flags.getBooleanValue('flag-2', context);
const flag3 = await flags.getStringValue('flag-3', 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';4. Handle Errors Gracefully
Don't let flag evaluation failures break your app:
async function safeGetFlag(flagKey: string, context?: EvaluationContext): Promise<boolean> {
try {
return await flags.getBooleanValue(flagKey, context);
} catch (error) {
console.error(`Flag evaluation failed for ${flagKey}:`, error);
return false;
}
}Use Cases
Feature Rollout
Gradually roll out a feature to users:
const isEnabled = await flags.getBooleanValue('new-checkout', {
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', {
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', {
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', {
targetingKey: user.id,
beta_tester: user.isBetaTester,
});Kill Switches
Instantly disable features in production:
const isDisabled = await flags.getBooleanValue('disable-payments');
if (isDisabled) {
return <PaymentsDisabledPage />;
}