Open-Source Wikis

/

Bitwarden Server

/

Systems

/

Feature flags

bitwarden/server

Feature flags

Active contributors: every product team.

Purpose

IFeatureService is the runtime-toggle abstraction every team uses to gate new code. In the cloud it is backed by LaunchDarkly; in self-host it returns the static defaults defined in code (or in the operator's appsettings.SelfHosted.json).

Directory layout

src/Core/Services/
├── IFeatureService.cs                  # interface
└── Implementations/
    ├── LaunchDarklyFeatureService.cs   # cloud
    ├── ConfigurationFeatureService.cs  # self-host: reads from appsettings
    └── NoopFeatureService.cs           # tests / minimal hosts

src/Core/Constants.cs                   # FeatureFlagKeys constants live here

Adding a flag

  1. Add a public const string My_Flag = "my-flag"; to the FeatureFlagKeys static class in src/Core/Constants.cs. The string value is the LaunchDarkly key; the C# constant should be PascalCase.
  2. Inject IFeatureService and call IsEnabled(FeatureFlagKeys.My_Flag, currentContext).
  3. Configure the flag in LaunchDarkly with a default value of false. Cloud rollout will flip it on.
  4. For self-host, the flag returns the LaunchDarkly default (currently coded as false for unknown keys); admins can override per-flag via globalSettings.LaunchDarkly configuration if needed.

CI workflow .github/workflows/code-references.yml exports a list of every FeatureFlagKeys.X reference and feeds it back to LaunchDarkly so unused flags can be culled.

Removing a flag

Once a flag is fully rolled out:

  1. Inline the "true" branch and delete the alternate code path.
  2. Remove the constant from FeatureFlagKeys.
  3. Delete the LaunchDarkly definition.
  4. Search git log --grep "remove feature flag" for prior examples — the team typically titles these PRs chore(<team>): remove unused FF.

How it works

graph LR
    Code["if (await _ff.IsEnabledAsync(FeatureFlagKeys.X, ctx))"] --> Service[IFeatureService]
    Service --> LD[LaunchDarklyFeatureService\n(cloud)]
    Service --> Cfg[ConfigurationFeatureService\n(self-host)]
    Service --> Noop[NoopFeatureService\n(tests)]

IFeatureService.IsEnabledAsync(key, ctx) evaluates the flag with a LaunchDarkly context built from the current user / org / environment. Targeting can therefore key off org id, user id, plan type, country, etc. The current-context-to-LD-context mapping lives in LaunchDarklyFeatureService.BuildContext.

Conventions

  • Flags follow kebab-case in LaunchDarkly (bulk-auto-confirm-on-login) and PascalCase in C# (BulkAutoConfirmOnLogin).
  • Keep flags short-lived. The codebase has accumulated many "remove flag" PRs (e.g. PR #7549 "chore(billing): remove unused FF"); aim to retire flags within a release or two.
  • Prefer feature flags over branch toggles — they keep PRs small and deployable.

Entry points for modification

  • Add a new flag → see steps above.
  • Switch to a different provider → implement IFeatureService and register it conditionally in services.AddDefaultServices.
  • Add a flag-aware authorization handler → inject IFeatureService and short-circuit the handler if the flag is off.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Feature flags – Bitwarden Server wiki | Factory