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_FLAGStable — one row per feature key (FeatureKeyunique),Enableddefaulting to 0/off. A brand-new Eos-owned table, hand- written entity/config outsidePersistence/Scaffolded/(same precedent asEOS_IDEMPOTENCY_KEYS/EOS_AUDIT_LOG).UpdatedDtis an optimistic- concurrency token, same mechanism asCity/AppSetting.IFeatureFlagRepository(EntityFrameworkCorewrite strategy) —IsEnabledAsync(featureKey)is the hot-path read every gating call site should use: cached (1hr TTL, same lookup-table caching rule asAppSettingRepository), 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/DeleteAsyncback the admin edit page's own live (uncached) CRUD.[RequiresFeatureFlag("Key")]+FeatureFlagActionFilter(registered globally alongsideValidationActionFilter) — same "no-op unless the action opts in" shape asValidationActionFilter/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, injectIFeatureFlagRepositorydirectly instead of adding this attribute.- Admin CRUD:
FeatureFlagsController(api/v1/operations/featureflags) — mechanism 1 only, noTerritoryFilter, 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.