Feature flags

How the kit switches features on per organization: flags with a default, a rollout share and per-organization overrides, read by code and by the web, managed by super-admins.

A feature flag turns a feature on for the platform, for a share of the organizations, or for chosen ones. Code asks IFeatureFlags, the web reads every flag of the organization at once, and super-admins manage the flags.

How a flag is decided

For an organization, a flag is:

  1. the organization's override, when there is one;
  2. otherwise on when the flag is enabled and the organization's bucket is below the flag's rollout share.

The bucket is a number from 0 to 99 drawn from the flag's key and the organization's id (SHA-256), the same on every replica and every run. A rollout of 20% therefore reaches the same fifth of the organizations until it changes, and raising it only adds organizations. An unknown flag is off.

Using a flag in code

if (await featureFlags.IsEnabledAsync("reports.export", organizationId, cancellationToken))
{
    // the feature
}

An organization endpoint that belongs to a flagged feature declares it, and answers 404 while the flag is off, so the feature's existence is not revealed:

group.MapGet("/reports/export", ExportReports.HandleAsync).RequireFeature("reports.export");

The web

GET /v1/organizations/{organizationId}/features returns { "features": { "reports.export": true, ... } } to any member: every flag with its value for the organization.

Managing flags

Super-admins, with a second factor, under /v1/admin/feature-flags:

EndpointDoes
GET /v1/admin/feature-flagsThe flags with their default, rollout and number of overrides.
POST /v1/admin/feature-flags with { "key": "reports.export", "description": "...", "enabled": true, "rolloutPercentage": 20 }Creates a flag. The key uses lowercase letters and digits separated by single dots or dashes, up to 64 characters; the rollout defaults to 100.
GET /v1/admin/feature-flags/{key}A flag with the organizations that override it.
PATCH /v1/admin/feature-flags/{key} with any of description, enabled, rolloutPercentageChanges a flag.
PUT /v1/admin/feature-flags/{key}/overrides/{tenantId} with { "enabled": true }Turns the flag on or off for one organization.
DELETE /v1/admin/feature-flags/{key}/overrides/{tenantId}Returns the organization to the flag's default.

Cache and audit

The flags are cached under one tag and each organization's overrides under its own key, for ten minutes. A change drops them at once on every replica sharing the Redis cache, so the next read sees it. A flag is platform-wide: its creation and changes go to the operators' log with the super-admin. An override is a change in one organization and is recorded in its audit log (feature_override.created, .updated, .deleted).