Skip to main content

Per-client feature flags (EOS_FEATURE_FLAGS)

Shipped 2026-08-25 — lets a feature be enabled/disabled per client without a code branch or a separate deploy. Because each client already gets its own isolated Api+Workers deployment pointed at its own database (see Multi-Client Deployment), per-client here simply means per-database — there's no cross-client registry, percentage rollout, user/cohort targeting, or dynamic external flag service (no LaunchDarkly-style mechanism is needed or built).

Building blocks

  • EOS_FEATURE_FLAGS table — one row per feature key (FeatureKey unique), Enabled defaulting to 0/off. A brand-new Eos-owned table, hand- written entity/config outside Persistence/Scaffolded/ (same precedent as EOS_IDEMPOTENCY_KEYS/EOS_AUDIT_LOG). UpdatedDt is an optimistic- concurrency token, same mechanism as City/AppSetting.
  • IFeatureFlagRepository (EntityFrameworkCore write strategy) — IsEnabledAsync(featureKey) is the hot-path read every gating call site should use: cached (1hr TTL, same lookup-table caching rule as AppSettingRepository), and defaults an unknown key to disabled rather than throwing — gating code must never crash just because nobody has created the flag row yet. GetByKeyAsync/ListPagedAsync/CreateAsync/ UpdateAsync/DeleteAsync back the admin edit page's own live (uncached) CRUD.
  • [RequiresFeatureFlag("Key")] + FeatureFlagActionFilter (registered globally alongside ValidationActionFilter) — same "no-op unless the action opts in" shape as ValidationActionFilter/IdempotencyActionFilter. An action carrying this attribute returns 404 (deliberately not 403 — a not-yet-licensed feature shouldn't confirm its own existence to a caller who doesn't have it) when the named flag isn't enabled for this deployment. Layers on top of whatever [RequiresMenuPermission]/[Authorize] gate already exists — every gate present must pass, not just this one. For gating inside business logic rather than a whole endpoint, inject IFeatureFlagRepository directly instead of adding this attribute.
  • Admin CRUD: FeatureFlagsController (api/v1/operations/featureflags) — mechanism 1 only, no TerritoryFilter, same bucket as City/Country/Choices/SysConfig/AppSettings (see Authorization Model). Menu seeded with no role grant by default — grant sparingly, per client.

:::note Built ahead of any concrete feature that needs it Same "mechanism before the first real consumer" precedent as the domain-layer state-machine pattern and the Idempotency-Key pattern. The next feature that needs staged per-client rollout adopts this by adding one attribute (or one repository call) and creating that client's flag row — not by building the mechanism itself. :::

:::danger Do not Migration scripts creating the table/menu row have not been run against any real database yet — review before applying, per client, same as every other schema change in this repo. :::

Reference files

EosFeatureFlag.cs, EosFeatureFlagConfiguration.cs, FeatureFlagDto.cs, FeatureFlagDtoValidator.cs, IFeatureFlagRepository.cs, FeatureFlagRepository.cs, RequiresFeatureFlagAttribute.cs, FeatureFlagActionFilter.cs, FeatureFlagsController.cs.