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

Packages

PackageDescriptionInstall
@kitbase/flagsCore SDK (vanilla JS) — also available via CDNnpm install @kitbase/flags
@kitbase/flags-reactReact hooks & providernpm install @kitbase/flags-react
@kitbase/flags-angularAngular signals & servicesnpm install @kitbase/flags-angular

Quick Start

typescript
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' });
tsx
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'} />;
}
typescript
// 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');
}
dart
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();
php
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.

html
<script src="https://unpkg.com/@kitbase/flags/dist/index.global.js"></script>
html
<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.

html
<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:

html
<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:

html
<script src="https://unpkg.com/@kitbase/flags@0.2.0/dist/index.global.js"></script>

Configuration

typescript
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

OptionTypeDefaultDescription
sdkKeystringrequiredYour Kitbase SDK key
baseUrlstring'https://api.kitbase.dev'API base URL (for self-hosting)
defaultValuesRecord<string, unknown>{}Global fallback values for disabled/missing flags
localEvaluationLocalEvaluationConfigLocal evaluation settings (see below)
remoteEvaluationCacheRemoteEvaluationCacheConfigCache settings for remote evaluation
onConfigurationChange(config) => voidCallback when config updates (local eval only)
onError(error) => voidCallback on errors (local eval only)

Local Evaluation Options

OptionTypeDefaultDescription
enabledbooleanfalseEnable local evaluation mode
refreshIntervalSecondsnumber60Polling interval in seconds (0 to disable)
initialConfigurationFlagConfigurationBootstrap config for SSR/offline

Remote Evaluation Cache Options

OptionTypeDefaultDescription
ttlnumber60000Cache TTL in milliseconds
persistentbooleantrue in browserUse localStorage for persistent caching

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
const isEnabled = await flags.getBooleanValue('dark-mode');

// With evaluation context
const isEnabled = await flags.getBooleanValue('dark-mode', {
  targetingKey: 'user-123',
  plan: 'premium',
});

String Flags

typescript
const variant = await flags.getStringValue('checkout-flow', context);

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

Number Flags

typescript
const rateLimit = await flags.getNumberValue('api-rate-limit', context);
const discount = await flags.getNumberValue('promo-discount', context);

JSON Flags

typescript
const config = await flags.getJsonValue('feature-config', context);

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

Full Evaluation Details

Use evaluateFlag() to get full resolution details including reason, variant, and metadata:

typescript
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
typescript
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:

typescript
// 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:

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.

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.

typescript
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.

typescript
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.

typescript
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).

typescript
const key = flags.getTargetingKey(); // 'user-123' or null

Context Priority

The targetingKey used for evaluation is resolved in this order:

  1. Per-call contextflags.getBooleanValue('flag', { targetingKey: 'explicit' })
  2. Identity — set via flags.identify('user-123')
  3. Anonymous ID — auto-generated, persisted to localStorage

Resolution Details

The evaluateFlag() method returns full resolution information:

typescript
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

ReasonDescription
STATICFlag has a static value (no rules)
DEFAULTDefault value returned (no rules matched)
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";
}

React SDK

The @kitbase/flags-react package provides React hooks and a context provider for feature flag evaluation.

Installation

bash
npm install @kitbase/flags-react
bash
pnpm add @kitbase/flags-react
bash
yarn add @kitbase/flags-react

Provider Setup

Wrap your app with FlagsProvider:

tsx
import { FlagsProvider } from '@kitbase/flags-react';

function App() {
  return (
    <FlagsProvider config={{
      sdkKey: '<YOUR_SDK_KEY>',
      defaultValues: { 'dark-mode': false },
    }}>
      <YourApp />
    </FlagsProvider>
  );
}

For local evaluation:

tsx
<FlagsProvider config={{
  sdkKey: '<YOUR_SDK_KEY>',
  localEvaluation: { enabled: true, refreshIntervalSeconds: 60 },
}}>
  <YourApp />
</FlagsProvider>

Hooks

useBooleanFlag(flagKey, options?)

tsx
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?)

tsx
function Checkout() {
  const { data: variant } = useStringFlag('checkout-variant', {
    context: { targetingKey: userId },
  });

  return <Checkout variant={variant} />;
}

useNumberFlag(flagKey, options?)

tsx
function Cart() {
  const { data: maxItems } = useNumberFlag('max-cart-items', {
    context: { targetingKey: userId },
  });

  return <Cart maxItems={maxItems} />;
}

useJsonFlag<T>(flagKey, options?)

tsx
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:

tsx
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

OptionTypeDefaultDescription
contextEvaluationContextEvaluation context for targeting
refetchOnContextChangebooleantrueRe-evaluate when context changes

Hook Return Type

All flag hooks return UseFlagResult<T>:

FieldTypeDescription
dataT | undefinedThe flag value
isLoadingbooleanWhether the flag is being evaluated
errorError | nullAny 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

bash
npm install @kitbase/flags-angular
bash
pnpm add @kitbase/flags-angular
bash
yarn add @kitbase/flags-angular

Provider Setup

Register the provider in your app config:

typescript
// 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:

typescript
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>>:

FieldTypeDescription
valueT | undefinedThe flag value (undefined while loading)
isLoadingbooleanWhether the flag is currently loading
errorError | nullError if flag evaluation failed

Observable-based API

Flags as RxJS Observables that re-emit when the flag value changes:

typescript
@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:

typescript
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:

ObservableDescription
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):

typescript
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)

typescript
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:

typescript
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)

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_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.

typescript
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}`);
  }
}
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}";
}

Error Types

ErrorDescriptionProperties
FlagsErrorBase class for all flag errorsmessage
AuthenticationErrorInvalid SDK key
ApiErrorAPI returned an errorstatusCode, response
ValidationErrorInvalid inputfield
TimeoutErrorRequest timed out
FlagNotFoundErrorFlag doesn't existflagKey
TypeMismatchErrorWrong type requestedflagKey, expectedType, actualType
InvalidContextErrorInvalid evaluation context
ParseErrorFailed 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:

typescript
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:

typescript
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:

typescript
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();
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/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:

typescript
// 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

FeatureRemote EvaluationLocal Evaluation
ConfigurationDefaultlocalEvaluation: { enabled: 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

Best Practices

1. Use Default Values

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

typescript
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:

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. Use Flag Snapshots for Multiple Flags

When evaluating multiple flags, use snapshots for better performance:

typescript
// 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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

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

Kill Switches

Instantly disable features in production:

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

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

Released under the MIT License.