Feature-flag registry refactor

Scope

Compared refactor/centralize-feature-flag-registry with develop.

The short version

The branch changes feature flags from many module-local files merged at build time to one complete central file per variant.

The intended result is:

For a selected variant, every module sees the same feature values. Terraform and UI builds use the same source of truth as backend builds.

The central source is p8-variation-values/features/.

The design is simpler and easier to change, but this branch also exposes a few problems that should be fixed or consciously accepted before merging:

  1. The main build orchestrator only loads flags when ../features already exists. A clean checkout no longer has that directory, so deployment-gated modules such as the file converter can be skipped incorrectly (scripts/deployments/build_and_deploy.sh:803-806).
  2. P8_FeatureFirmInAccount was removed from every registry although the UI still uses it. The validator does not detect the omission because it misses REACT_APP_P8_... references.
  3. System-manager Terraform generation no longer copies P8_DEPLOYMENT_TYPE into the shell variable used by envsubst, so its operation-cycle Lambdas can receive an empty deployment type.
  4. Centralization intentionally removes module-specific overrides, but several effective values changed. Those changes need confirmation.

Old model: two files and load order

Before this branch, a build commonly did this:

  1. Load the matching file from p8-variation-values/features/.
  2. Load a second matching file from the component’s own features/ directory.
  3. Let the second file override the first when the same variable appeared twice.
  4. Write the combined result to features/p8_features.conf.

The old implementation is visible in the develop version of p8-deployments/scripts/build_utils.sh and p8-deployments/scripts/common.sh.

This allowed a component to disagree with another component for the same variant. It also made behavior depend on symlinks, file presence, and load order. For example, the matching engine had a CIX-local override for P8_ProductVariant, and API/system-manager files had their own flag values.

New model: one complete registry

There are eight authoritative variant files:

p8-variation-values/features/
  p8_bpx_features.conf
  p8_carbon_features.conf
  p8_cix_features.conf
  p8_crypto_features.conf
  p8_da_features.conf
  p8_default_features.conf
  p8_imperium_features.conf
  p8_prediction_features.conf

Each file has the same 93 assignments. Only values differ. For example, CIX deliberately has:

P8_ProductVariant=carbon
P8_SolutionVariant=cix

This matters because CIX is a solution variant built on the Carbon product variant.

The registry includes more than feature switches. It also includes deployment flags, risk flags, operation-cycle flags, UI flags, and variant identity values.

registry.csv is metadata, not runtime input (p8-variation-values/features/registry.csv:1-94):

name,owner,default
P8_FeatureAnnouncement,p8-api,true

It records who owns a flag and what the default value should be. The loader does not read the CSV; the validator does.

The shared loader

p8-variation-values/features/feature_flags.sh contains the only shared loader:

See p8-variation-values/features/feature_flags.sh:4-47.

Variant selection

The normal selection order is:

P8_SOLUTION_VARIATION
    otherwise P8_PRODUCT_VARIATION
        otherwise default

This is implemented in p8-deployments/scripts/build_utils.sh:153-154 and common.sh:467.

An unknown variant silently falls back to default. That preserves old behavior, but a typo can therefore deploy default configuration rather than fail immediately.

How each consumer now works

Backend/module builds

init_feature_flags in p8-deployments/scripts/build_utils.sh:151-185:

  1. Selects the solution/product/default variant.
  2. Finds the central registry through P8_FEATURE_REGISTRY_DIR or known repository-relative paths.
  3. Sources feature_flags.sh.
  4. Loads the selected central file.
  5. Writes it to the module’s generated features/p8_features.conf.

The generated file is a build/package artifact. It is not an independently edited source file.

common.sh::read_feature_info follows the same process (p8-deployments/scripts/common.sh:465-492).

p8-digital-asset-module/build_utils.sh is now a symlink to the shared implementation instead of a 532-line copy. This removes a second copy that could drift.

Terraform deployments

Twenty-seven deployments/tfvars.sh files now use the same three calls. For example, p8-api/p8-integrations/deployments/tfvars.sh:53-59:

source "$registry_dir/feature_flags.sh"
p8_load_feature_flags "$registry_dir" "${P8_SOLUTION_VARIATION:-$variant}"
p8_export_terraform_flags "$registry_dir/terraform-mappings.conf" "p8-api/p8-integrations"

terraform-mappings.conf has 97 rows for 27 consumers. A row has this shape:

consumer|source flag|Terraform variable|type
p8-api/p8-integrations|P8_FeatureIntegrations|enable_integrations|bool

The mapping layer is explicit. It does not guess Terraform names from flag names. It also:

  • fails if the source flag was not loaded;
  • validates true/false for bool mappings;
  • exports the exact target name expected by Terraform.

The processing engine intentionally maps several values as string, because its Terraform variables are string-typed (p8-variation-values/features/terraform-mappings.conf:80-86; p8-processing-engine/deployments/variables.tf:80-124).

Terraform packages and deploys

The packaging logic now copies the complete central feature directory, including the loader, registry metadata, and mappings (scripts/deployments/build_and_deploy.sh:501-503).

A packaged deployment uses the already-generated features/p8_features.conf; it does not try to rebuild the registry from a source checkout (p8-deployments/scripts/build_utils.sh:156-167). This is the right separation:

  • build/publish: resolve the variant and generate the package file;
  • install/deploy: consume the package file.

UI builds

Both UI scripts now call the shared loader and convert the generated environment variables from P8_* to REACT_APP_P8_*:

  • p8-trader-ui/scripts/p8_manage_feature_files.sh:15-36
  • p8-ui/scripts/p8_manage_feature_files.sh:17-58

The flow is:

central p8_<variant>_features.conf
    -> generated features/p8_features.conf
    -> variant .env file
    -> P8_* renamed to REACT_APP_P8_*
    -> webpack/rspack feature elimination

Variant-specific .env files are still appended after the central file. That means a UI .env file can still override a central value even though the new documentation says module-local overrides are prohibited.

What was deleted or simplified

The branch deletes 113 module-local variant feature files from components including API, auth, custody, file manager, post-trade, processing, refdata, system-manager, tools, and deployment scripts.

It also removes the feature-file include patterns from 20 module_conf.yaml files. The module configurations still decide whether a module supports a variant and whether an allowed_on_condition flag enables it; they no longer provide the flag values themselves.

Examples:

The file converter still uses a feature condition:

allowed_on_condition: "P8_FeatureDocxToPdfConverter"

The condition remains, but its value must now come from the central registry.

Two old include patterns remain:

They look like remnants of older conventions and are not part of the new lowercase central registry model.

System-manager operational configuration

The old system-manager feature files contained both flags and ordinary Kafka/output configuration. This branch separates those concepts.

These values moved into per-variant YAML files:

p8-system-manager/p8-system-manager-engine/configs/*-config.yaml

The new files contain:

p8-system-manager/p8-system-manager-engine/deployments/tfvars.sh:60-65 reads the output settings from YAML. This is a good separation: feature flags remain in the registry, while operational configuration stays with system-manager.

Important behavior changes and review findings

1. Clean checkout can skip conditionally deployed modules

scripts/deployments/build_and_deploy.sh:803-806 contains:

if [ -d "../features" ]; then
  pushd ../
  init_feature_flags $mode
  popd
fi

The branch deletes tracked variant files and the features directory from many modules. On a clean checkout, ../features may not exist yet, so init_feature_flags is never called.

is_deployable_module then checks an unset feature variable (scripts/deployments/build_and_deploy.sh:731-747). For example, p8-file-manager/module_conf.yaml gates p8-file-converter on P8_FeatureDocxToPdfConverter, which is true for Carbon and CIX in the central registry. With no loaded environment variable, the converter can be treated as disabled.

The guard should be based on the central loader being available, or the orchestrator should always initialize flags before checking module conditions.

2. P8_FeatureFirmInAccount is missing but still used

develop had this flag in the central files. The current registry.csv and all eight current variant files do not contain it.

The UI still contains feature-controlled branches, for example:

  • p8-ui/packages/p8-admin-ui-core/src/hooks/participants/useFirmAccounts.ts:20-32
  • p8-trader-ui/src/products/trader/services/AccountBalanceService/impl/AccountBalanceServiceImpl.ts:240-248
  • p8-trader-ui/src/products/admin/pages/sub-pages/RefData/EntityView/ParticipantsEntityView/FirmAccounts.tsx:221-229

Carbon and CIX previously set this flag to true; DA and most other variants used false.

The new validator misses this because its regular expression looks for a word boundary before P8_ (validate.py:15). In REACT_APP_P8_..., the preceding underscore is itself a word character. The validator also does not scan .env files.

This flag should be restored with per-variant values, or all consumers should be deliberately removed/rewritten.

3. deployment_type can become empty in system-manager Terraform

The old system-manager loader explicitly did:

export deployment_type="${P8_DEPLOYMENT_TYPE:-}"

The new loader removes that assignment (p8-system-manager/p8-system-manager-engine/deployments/tfvars.sh:51-65), but the generated Terraform file still contains:

deployment_type="$deployment_type"

at lines 201 and 213.

The shared provision script separately exports TF_VAR_deployment_type, but an explicit empty terraform.tfvars value takes precedence over that environment variable. The value is consumed by operation-cycle Lambdas as P8_DEPLOYMENT_TYPE (operation_cycle.tf:151-171 and later entries).

The old passthrough needs to be preserved or the Terraform template needs to use the shared variable consistently.

4. Some variant/module overrides changed effective behavior

Centralization removes overrides by design. The important differences found during comparison are:

  • CIX P8_EnablePasswordExpiration: true in the old API/system-manager local files, now false in p8-variation-values/features/p8_cix_features.conf:24. This gates the password-expiration Lambda and schedule.
  • Imperium P8_FeatureMarketDataUpdate: old API-local value false, current central value true (p8-variation-values/features/p8_imperium_features.conf:69). This now enables the market-data update API path.
  • Imperium P8_FeatureFileOperations: old API-local value false, current central value true (p8-variation-values/features/p8_imperium_features.conf:57).
  • CIX P8_ProductVariant: old matching-engine override was cix; current central value is carbon, while P8_SolutionVariant remains cix.

These may be intended cleanup, but they are behavior changes rather than mechanical file moves.

5. Old documentation is contradictory

Some documentation still describes per-module overrides:

  • documentation/architecture.md:1499
  • documentation/components.md:180
  • p8-deployments/scripts/AGENTS.md:226-234 has the new model, but other nearby guidance still assumes local feature files.
  • .agents/skills/product/add-variant/SKILL.md:103 still instructs contributors to create module-local feature files.

A future contributor following the old variant skill can immediately reintroduce the problem this branch is trying to remove.

How to change things after this branch

Change an existing flag value

Edit only the relevant central file:

p8-variation-values/features/p8_<variant>_features.conf

Do not edit generated features/p8_features.conf files. They are recreated by builds.

Example:

# p8_cix_features.conf
P8_FeatureMarketDataUpdate=true

Remember that this one value is now shared by every consumer of the CIX registry.

Add a new flag

  1. Add one assignment to all eight p8_*_features.conf files.
  2. Add one row to registry.csv with the owner and default.
  3. Search all consumers and use the exact same flag spelling.
  4. Run the validator and shell tests.

The validator requires every variant to have the same key set and requires the default metadata value to match p8_default_features.conf (validate.py:34-106).

Add a Terraform consumer

  1. Declare the Terraform variable in that consumer’s deployments/*.tf.
  2. Add a row to terraform-mappings.conf:
consumer|P8_SourceFlag|terraform_variable|bool
  1. Ensure that consumer’s tfvars.sh calls p8_export_terraform_flags with the exact consumer name.
  2. Do not add hand-written flag-to-Terraform conversion logic to the consumer.

Use string instead of bool only when the Terraform variable is intentionally string-typed.

Add or rename a variant

The current branch hard-codes the supported variant set in validate.py:14:

VARIANTS = {"bpx", "carbon", "cix", "crypto", "da", "default", "imperium", "prediction"}

A new variant therefore needs, at minimum:

  • a new central registry file;
  • the variant added to VARIANTS;
  • matching metadata/default values;
  • module variants: participation where appropriate;
  • UI/build variant files and Maven/Node variant configuration;
  • per-variant system-manager config where needed;
  • tests for loading and packaging.

Do not create module-local feature files for the new variant.

Validation performed

The branch’s new checks pass locally:

python3 p8-deployments/tools/feature_flag_tools/validate.py
  Feature flag registry is valid

python3 p8-deployments/tools/feature_flag_tools/test_validate.py
  6 tests passed

bash p8-deployments/scripts/tests/feature_flags_test.sh
  feature flag loader tests passed

bash -n on changed shell scripts
  passed

git diff --check
  passed

The tests cover registry shape, loading, default fallback, Terraform mapping mechanics, and all eight variant loads. They do not cover UI builds, the clean-checkout orchestrator guard, system-manager Terraform output, or preservation of module-specific behavior.

File map

File Role
p8-variation-values/features/p8_<variant>_features.conf Authoritative values
p8-variation-values/features/registry.csv Owner/default metadata used by validation
p8-variation-values/features/terraform-mappings.conf Explicit Terraform consumer mappings
p8-variation-values/features/feature_flags.sh Shared loader and Terraform exporter
p8-deployments/scripts/build_utils.sh Build-time loading and generated package file
p8-deployments/scripts/common.sh Main deployment/build helper’s registry loading
scripts/deployments/build_and_deploy.sh Module selection, packaging, and deploy orchestration
*/deployments/tfvars.sh Per-consumer Terraform variable generation
p8-deployments/tools/feature_flag_tools/validate.py Registry/reference/mapping validation
p8-deployments/scripts/tests/feature_flags_test.sh Shell integration coverage

Bottom line

The architectural idea is straightforward and worthwhile: one variant file, one loader, explicit Terraform mappings, and validation in CI. The safest way to work with it is to treat p8-variation-values/features/ as the only editable feature source and remember that one value now affects every module.

Before relying on the branch, resolve the clean-checkout loading guard, restore or remove P8_FeatureFirmInAccount deliberately, preserve deployment_type, and confirm the listed CIX/Imperium value changes.