OpenFeature Dart Server SDK

Specification Release Built with Dart
Pub Version API Reference Code Coverage GitHub CI Status

Warning: this repository is still an in-progress implementation of the dart-server-sdk.

OpenFeature is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool.

Quick start

Requirements

Dart language version: 3.11.4

Note

The OpenFeature Dart Server SDK only supports the latest currently maintained Dart language versions.

Install

dependencies:
  openfeature_dart_server_sdk: ^0.0.21

Then run

dart pub get

Usage

import 'package:openfeature_dart_server_sdk/feature_provider.dart';
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';

void main() async {
  final api = OpenFeatureAPI();

  await api.setProviderAndWait(
    InMemoryProvider({
      'new-feature': true,
      'welcome-message': 'Hello, OpenFeature!',
    }),
  );

  final client = api.getClient('my-app');
  final newFeatureEnabled = await client.getBooleanFlag(
    'new-feature',
    defaultValue: false,
  );

  final details = await client.getBooleanDetails(
    'new-feature',
    defaultValue: false,
  );
  print('Reason: ${details.reason}, Variant: ${details.variant}');

  if (newFeatureEnabled) {
    final welcomeMessage = await client.getStringFlag(
      'welcome-message',
      defaultValue: 'Welcome!',
    );
    print(welcomeMessage);
  }
}

API reference

See pub.dev/documentation/openfeature_dart_server_sdk/latest for the complete API documentation.

Features

Status Feature Description
Providers Integrate with a commercial, open source, or in-house feature management tool.
Targeting Contextually-aware flag evaluation using evaluation context.
Hooks Add functionality to various stages of the flag evaluation life-cycle.
Tracking Associate user actions with feature flag evaluations for experimentation.
Logging Integrate with popular logging packages.
Domains Logically bind clients with providers.
⚠️ Multi-Provider Compose multiple providers behind one SDK-level provider with a selection strategy.
Eventing React to state changes in the provider or flag management system.
Shutdown Gracefully clean up a provider during application shutdown.
Transaction Context Propagation Set a specific evaluation context for a transaction such as an HTTP request or thread.
Extending Extend OpenFeature with custom providers and hooks.

Implemented: ✅ | Experimental: ⚠️ | Not implemented yet: ❌

Providers

Providers are an abstraction between a flag management system and the OpenFeature SDK. Look here for a complete list of available providers. If the provider you need does not exist yet, see Develop a provider.

final api = OpenFeatureAPI();
api.setProvider(MyProvider());

Targeting

Sometimes the value of a flag must consider dynamic criteria about the application or user, such as location, IP, or organization. In OpenFeature this is called targeting. If your flag management system supports targeting, you can provide the input data using the evaluation context.

final api = OpenFeatureAPI();
api.setGlobalContext(
  OpenFeatureEvaluationContext({
    'region': 'us-east-1-iah-1a',
  }),
);

final client = FeatureClient(
  metadata: ClientMetadata(name: 'my-app'),
  hookManager: HookManager(),
  defaultContext: const EvaluationContext(
    attributes: {
      'version': '1.4.6',
    },
  ),
  provider: api.provider,
);

final result = await client.getBooleanFlag(
  'feature-flag',
  defaultValue: false,
  context: const EvaluationContext(
    attributes: {
      'user': 'user-123',
      'company': 'Initech',
    },
  ),
);

Hooks

Hooks allow custom logic to be added at well-defined points of the flag evaluation life-cycle. Look here for a complete list of available hooks.

Once you have added a hook dependency, it can be registered at the global or client level.

final api = OpenFeatureAPI();
api.addHooks([MyGlobalHook()]);

final client = api.getClient('my-app');
client.addHook(MyClientHook());

Note

Invocation-level hooks are not yet supported. Hooks can currently be registered at the global or client level.

Before hooks may return context updates. Any returned attributes are merged into the evaluation context before the provider is called.

Note

Invocation-level hooks are not yet supported. Hooks can currently be registered at the global or client level.

Tracking

The tracking API allows you to associate user actions with feature flag evaluations. This is useful for experimentation and downstream analytics.

import 'package:openfeature_dart_server_sdk/evaluation_context.dart';
import 'package:openfeature_dart_server_sdk/feature_provider.dart';
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';

final api = OpenFeatureAPI();
final client = api.getClient('my-app');

await client.track(
  'checkout-completed',
  context: const EvaluationContext(
    attributes: {
      'user': 'user-123',
    },
  ),
  trackingDetails: const TrackingEventDetails(
    value: 99.99,
    attributes: {'currency': 'USD'},
  ),
);

Note that some providers may not support tracking. Check the provider documentation for details.

Logging

In accordance with the OpenFeature specification, the SDK does not generally log messages during flag evaluation.

The SDK uses the package:logging structured logging API internally. You can configure log levels and listeners to capture SDK output for troubleshooting and debugging.

For lifecycle-level logging, use the built-in LoggingHook or OpenTelemetryHook. LoggingHook redacts evaluation-context values by default and only emits context keys unless includeContext: true is set explicitly.

Domains

Clients can be assigned to a domain. A domain is a logical identifier that can be used to associate clients with a particular provider. If a domain has no associated provider, the default provider is used.

import 'package:openfeature_dart_server_sdk/feature_provider.dart';
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';

final api = OpenFeatureAPI();

api.setProvider(InMemoryProvider({'default-flag': true}));

api.registerProvider(CustomCacheProvider());
api.bindClientToProvider('cache-domain', 'CustomCacheProvider');

final defaultClient = api.getClient('default-client');
await defaultClient.getBooleanFlag('default-flag', defaultValue: false);

final cacheClient = api.getClient('cache-client', domain: 'cache-domain');
await cacheClient.getBooleanFlag('cached-flag', defaultValue: false);

Multi-Provider (experimental)

OpenFeature treats Multi-Provider as an SDK-level utility rather than behavior owned by any one provider. It is useful for migrations and fallback compositions where one client should consult more than one underlying provider.

This SDK includes an experimental MultiProvider implementation with a default FirstMatchStrategy. Providers are evaluated in order. A FLAG_NOT_FOUND result is treated as a miss and evaluation continues with the next provider. Initialization and connection fan out across all configured providers, and tracking is treated as best-effort fan-out.

import 'package:openfeature_dart_server_sdk/feature_provider.dart';
import 'package:openfeature_dart_server_sdk/multi_provider.dart';
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';

final api = OpenFeatureAPI();
final multiProvider = MultiProvider([
  InMemoryProvider({'new-feature': true}),
  InMemoryProvider({'fallback-feature': true}),
]);

await api.setProviderAndWait(multiProvider);

final client = api.getClient('my-app');
final enabled = await client.getBooleanFlag(
  'new-feature',
  defaultValue: false,
);

Treat the current Multi-Provider surface as experimental until the strategy API and documentation settle further.

Eventing

Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions. Initialization events (PROVIDER_READY on success, PROVIDER_ERROR on failure) are dispatched for every provider. Some providers support additional events such as PROVIDER_CONFIGURATION_CHANGED.

import 'package:openfeature_dart_server_sdk/open_feature_api.dart';
import 'package:openfeature_dart_server_sdk/open_feature_event.dart';

final api = OpenFeatureAPI();

api.events.listen((event) {
  if (event.type == OpenFeatureEventType.PROVIDER_CONFIGURATION_CHANGED) {
    print('Provider configuration changed: ${event.message}');
  }
});

Client-scoped handlers are also available. A client only receives events for its associated provider.

final client = api.getClient('my-app');
final sub = client.addHandler((event) {
  print('Client received event: ${event.type}');
});
await client.removeHandler(sub);

The SDK also exposes a global event bus for SDK-specific flag evaluation events.

import 'package:openfeature_dart_server_sdk/event_system.dart';

OpenFeatureEvents.instance.subscribe(
  (event) {
    print('Flag evaluated: ${event.data['flagKey']} = ${event.data['result']}');
  },
  filter: EventFilter(
    types: {OpenFeatureEventType.flagEvaluated},
  ),
);

Shutdown

The OpenFeature API provides mechanisms to perform cleanup of registered providers. This should only be called when your application is shutting down.

import 'package:openfeature_dart_server_sdk/open_feature_api.dart';
import 'package:openfeature_dart_server_sdk/shutdown_manager.dart';

final api = OpenFeatureAPI();

final shutdownManager = ShutdownManager();
shutdownManager.registerHook(
  ShutdownHook(
    name: 'provider-cleanup',
    phase: ShutdownPhase.PROVIDER_SHUTDOWN,
    execute: () async {
      await api.dispose();
    },
  ),
);

await shutdownManager.shutdown();

Transaction Context Propagation

Transaction context is a container for transaction-specific evaluation context, such as user id, user agent, or IP. Transaction context can be set where request-specific data is available and then automatically applied to flag evaluations inside that transaction.

import 'package:openfeature_dart_server_sdk/transaction_context.dart';

final transactionManager = TransactionContextManager();

final context = TransactionContext(
  transactionId: 'request-123',
  attributes: {
    'user': 'user-456',
    'region': 'us-west-1',
  },
);
transactionManager.pushContext(context);

await client.getBooleanFlag('my-flag', defaultValue: false);

await transactionManager.withContext(
  'transaction-id',
  {'user': 'user-123'},
  () async {
    await client.getBooleanFlag('my-flag', defaultValue: false);
  },
);

transactionManager.popContext();

Extending

Develop a provider

To develop a provider, create a new project and include the OpenFeature SDK as a dependency. This can live in a new repository or in the existing contrib repository.

import 'package:openfeature_dart_server_sdk/feature_provider.dart';

class MyCustomProvider implements FeatureProvider {
  @override
  String get name => 'MyCustomProvider';

  @override
  ProviderMetadata get metadata => ProviderMetadata(name: name);

  @override
  ProviderState get state => ProviderState.READY;

  @override
  ProviderConfig get config => ProviderConfig();

  @override
  Future<void> initialize([Map<String, dynamic>? config]) async {}

  @override
  Future<void> connect() async {}

  @override
  Future<void> shutdown() async {}

  @override
  Future<void> track(
    String trackingEventName, {
    Map<String, dynamic>? evaluationContext,
    TrackingEventDetails? trackingDetails,
  }) async {}

  @override
  Future<FlagEvaluationResult<bool>> getBooleanFlag(
    String flagKey,
    bool defaultValue, {
    Map<String, dynamic>? context,
  }) async {
    return FlagEvaluationResult(
      flagKey: flagKey,
      value: true,
      evaluatedAt: DateTime.now(),
      evaluatorId: name,
    );
  }

  @override
  Future<FlagEvaluationResult<String>> getStringFlag(
    String flagKey,
    String defaultValue, {
    Map<String, dynamic>? context,
  }) async {
    return FlagEvaluationResult(
      flagKey: flagKey,
      value: 'value',
      evaluatedAt: DateTime.now(),
      evaluatorId: name,
    );
  }

  @override
  Future<FlagEvaluationResult<int>> getIntegerFlag(
    String flagKey,
    int defaultValue, {
    Map<String, dynamic>? context,
  }) async {
    return FlagEvaluationResult(
      flagKey: flagKey,
      value: 42,
      evaluatedAt: DateTime.now(),
      evaluatorId: name,
    );
  }

  @override
  Future<FlagEvaluationResult<double>> getDoubleFlag(
    String flagKey,
    double defaultValue, {
    Map<String, dynamic>? context,
  }) async {
    return FlagEvaluationResult(
      flagKey: flagKey,
      value: 3.14,
      evaluatedAt: DateTime.now(),
      evaluatorId: name,
    );
  }

  @override
  Future<FlagEvaluationResult<Map<String, dynamic>>> getObjectFlag(
    String flagKey,
    Map<String, dynamic> defaultValue, {
    Map<String, dynamic>? context,
  }) async {
    return FlagEvaluationResult(
      flagKey: flagKey,
      value: {'key': 'value'},
      evaluatedAt: DateTime.now(),
      evaluatorId: name,
    );
  }
}

Built a new provider? Let us know so we can add it to the docs.

Develop a hook

To develop a hook, create a new project and include the OpenFeature SDK as a dependency. This can live in a new repository or in the existing contrib repository. Implement your own hook by using the hook interfaces exported by the SDK.

import 'package:openfeature_dart_server_sdk/hooks.dart';

class MyCustomHook extends BaseHook {
  MyCustomHook()
    : super(metadata: HookMetadata(name: 'MyCustomHook'));

  @override
  Future<Map<String, dynamic>?> before(HookContext context) async {
    print('Before evaluating flag: ${context.flagKey}');
    return {
      'requestSource': 'my-hook',
    };
  }

  @override
  Future<void> after(HookContext context) async {
    print('After evaluating flag: ${context.flagKey}, result: ${context.result}');
  }

  @override
  Future<void> error(HookContext context) async {
    print('Error evaluating flag: ${context.flagKey}, error: ${context.error}');
  }

  @override
  Future<void> finally_(
    HookContext context,
    EvaluationDetails? evaluationDetails, [
    HookHints? hints,
  ]) async {
    print('Finished evaluating flag: ${context.flagKey}');
  }
}

Built a new hook? Let us know so we can add it to the docs.

Testing

Use the InMemoryProvider to set flags for the scope of a test. Use OpenFeatureAPI.resetInstance() in tearDown to clean up between tests.

import 'package:openfeature_dart_server_sdk/feature_provider.dart';
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';
import 'package:test/test.dart';

void main() {
  late OpenFeatureAPI api;
  late InMemoryProvider testProvider;

  setUp(() async {
    api = OpenFeatureAPI();
    testProvider = InMemoryProvider({
      'test-flag': true,
      'string-flag': 'test-value',
    });
    await api.setProviderAndWait(testProvider);
  });

  tearDown(() {
    OpenFeatureAPI.resetInstance();
  });

  test('evaluates boolean flag correctly', () async {
    final client = api.getClient('test-client');
    final result = await client.getBooleanFlag(
      'test-flag',
      defaultValue: false,
    );
    expect(result, isTrue);
  });

  test('evaluates string flag correctly', () async {
    final client = api.getClient('test-client');
    final result = await client.getStringFlag(
      'string-flag',
      defaultValue: 'default',
    );
    expect(result, equals('test-value'));
  });
}

Support the project

Contributing

Interested in contributing? Take a look at the CONTRIBUTING guide.

Thanks to everyone that has already contributed

Pictures of the folks who have contributed to the project

Made with contrib.rocks.