Skip to main content

Principles

  • Flags default to local - no network required to evaluate them
  • A flag must never silently change behaviour - every flag state change is recorded with a version
  • Flags must not affect the determinism of the mana engine: given the same inputs, mana output must be identical regardless of flag state
  • Remote flags are a future extension, not a current dependency

Flag storage

Flags are stored locally in a dedicated Drift table, separate from user data.
class FeatureFlags extends Table {
  TextColumn get key => text()();
  BoolColumn get enabled => boolean().withDefault(const Constant(false))();
  IntColumn get version => integer()();         // incremented on every change
  DateTimeColumn get updatedAt => dateTime()(); // wall-clock time of change
}
The version column is the auditability requirement: no flag change can occur without version incrementing. This makes flag state traceable in crash reports and support sessions.

Flag evaluation

Flags are evaluated through a Domain service, never read from storage directly in UI or business logic.
abstract class IFeatureFlagService {
  bool isEnabled(Flag flag);
  Stream<bool> watch(Flag flag);
}

enum Flag {
  experimentalCheckInFlow,
  syncEnabled,
}
The Domain service wraps storage. UI and Domain logic call isEnabled - they have no knowledge of where flags are stored or how they are set.
Flags must not be read inside the mana calculation path. Mana engine inputs and outputs must be deterministic. If a flag affects mana behaviour, it must do so by selecting a different engine configuration at startup, not by branching inside the calculation.

Setting flags

During development, flags are toggled via a hidden debug screen (long-press on the app version label). In production, the debug screen is compiled out. Every write goes through the service, never directly to the table:
Future<void> setFlag(Flag flag, {required bool enabled}) async {
  await _db.into(_db.featureFlags).insertOnConflictUpdate(
    FeatureFlagsCompanion.insert(
      key: flag.name,
      enabled: Value(enabled),
      version: Value(await _nextVersion(flag)),
      updatedAt: Value(DateTime.now()),
    ),
  );
}

Remote flags (future)

When remote flag support is added:
  • Remote values are fetched and written into the same local table via the sync layer
  • Evaluation does not change - the service still reads from local storage
  • A source column (local | remote) distinguishes origin for auditability
  • Remote flags must not be evaluated before the first successful fetch - local defaults apply until then
This keeps the evaluation path stable and offline-safe regardless of whether remote flags are active.