feat.so


feat Dart & Flutter SDK

Dart and Flutter client for feat feature flags.

feat evaluates flags server side for client keys and returns resolved values only. This SDK is a thin client over those remote-evaluation endpoints: it fetches resolved values and, optionally, holds a live stream for near-instant updates. It does no local evaluation and ships no bucketing engine.

The core package is pure Dart (no Flutter dependency), so it runs the same in Flutter apps and in Dart backends or CLIs. Flutter-only conveniences (a shared_preferences-backed store and a lifecycle observer) are opt-in glue - see Flutter integration.

Install

dependencies:
  feat_sdk: ^0.1.0

Usage

import 'package:feat_sdk/feat_sdk.dart';

final client = await FeatClient.initialize(FeatClientConfig(
  apiKey: 'feat_cs_...',                          // client-side id key
  context: EvalContext.user('user-123', attributes: {'plan': 'pro'}),
));

final enabled  = client.getBool('checkout-v2', false);   // sync
final greeting = client.getString('hero-greeting', 'Hi');
final ratio    = client.getNumber('rollout-ratio', 0);
final config   = client.getObject('theme', const {});
final detail   = client.getDetail('checkout-v2', defaultValue: false);
// detail.value / detail.variationId / detail.reason

Use a client-side id key (feat_cs_...) or a mobile key. Both are non-secret client keys and safe to ship in an app; never embed a server key.

initialize() returns once the first evaluation lands, or after initializeTimeout (default 5s) elapses - whichever comes first. On timeout the client is still usable and serves cached or default values until the network responds.

Reacting to changes

final sub = client.changes.listen((change) {
  print('${change.flagKey}: ${change.oldValue} -> ${change.newValue}');
});

await client.setContext(EvalContext.user('user-456'));

changes emits once per flag whose resolved value flips, after either a context change, a poll, or a streamed update. setContext() re-polls and reconnects the stream so the server evaluates against the new context.

Live streaming

The SDK can hold a Server-Sent Events connection so changes land near-instantly instead of waiting for the next poll. On connect the server sends a full snapshot (put); subsequent changes arrive as incremental patch frames. Updates are adopted in version order (only strictly newer versions win).

By default streaming follows your subscription: the stream opens when the first changes listener is added and closes when the last one is removed, so code that never listens pays nothing. Override with streaming:

FeatClientConfig(apiKey: k, streaming: StreamingMode.always);           // always
FeatClientConfig(apiKey: k, streaming: StreamingMode.never);            // never
FeatClientConfig(apiKey: k, streaming: StreamingMode.followSubscription); // default

Polling stays on as a safety net in every mode. While the stream is connected the poll relaxes to a slow (~10 min) backstop; the instant the stream drops it snaps back to the normal interval, so a dropped stream self-heals quickly. The stream reconnects on its own with jittered exponential backoff.

Anonymous contexts

Let the SDK mint a stable per-install identifier instead of supplying a context:

final client = await FeatClient.initialize(FeatClientConfig(
  apiKey: 'feat_cs_...',
  anonymous: true,
  storage: myStorage, // persist the key across restarts (see below)
));

This produces { targetingKey: uuid, user: { key: uuid, anonymous: true } }. With the default in-memory storage the key is stable for the process only; pass a persistent FeatStorage to keep it across restarts.

Offline

The last evaluated snapshot is persisted per context and served on the next start before the network responds, so a cold launch renders real values immediately. This uses the same pluggable FeatStorage; the default is in-memory (no persistence).

class MyStorage implements FeatStorage {
  @override Future<String?> read(String key) async { /* ... */ }
  @override Future<void> write(String key, String value) async { /* ... */ }
  @override Future<void> delete(String key) async { /* ... */ }
}

Flutter integration

The Flutter glue lives in the example (example/lib/feat_flutter.dart); copy it into your app or adapt it. It provides:

  • SharedPreferencesFeatStorage - a FeatStorage backed by shared_preferences for cross-restart persistence.
  • FeatLifecycleObserver - a WidgetsBindingObserver that pauses live updates when the app is backgrounded and resumes on foreground.
final storage = await SharedPreferencesFeatStorage.create();
final client = await FeatClient.initialize(
  FeatClientConfig(apiKey: 'feat_cs_...', anonymous: true, storage: storage),
);
final observer = FeatLifecycleObserver(client)..attach();

If you prefer not to use a WidgetsBindingObserver, call client.pause() and client.resume() (or client.handleAppLifecycleState(...)) directly.

Configuration

Option Default Notes
apiKey (required) Client-side id (feat_cs_...) or mobile key.
baseUrl https://data-01.feat.so Must be https:// (loopback http:// allowed for tests).
context none Initial context; or set later via setContext.
anonymous false Mint a stable anonymous context when no context is given.
storage InMemoryStorage Anonymous key + offline snapshot persistence.
pollInterval 30s Floored at 5s. Primary refresh when not streaming.
safetyNetPollInterval 10m Relaxed cadence while the stream is connected.
streaming followSubscription always / never / followSubscription.
initializeTimeout 5s Max wait for the first evaluation.

How it works

  • POST /sdk/v1/evaluate with { "context": ... } returns { "flags": { key: { value, variationId, reason } }, "version": int }. 404 (no datafile yet), 403, and 429 are treated as "no change".
  • GET /sdk/v1/eval/stream?key=...&context=<base64url(JSON(context))> is a Server-Sent Events stream of put (full snapshot) and patch (incremental) frames, adopted in version order.
  • Every request carries an X-Feat-Sdk: dart/<version> header.
  • Resolved values are cached so the typed getters are synchronous.

License

MIT

Libraries

feat_sdk
A thin Dart/Flutter client for the feat feature-flag platform.