Skip to content

Managing Feature Flags in the Angular App

This guide explains how to add, modify, and use feature flags in the SyRF web (Angular) application using the schema-driven code generator.

Overview

The feature flags system uses env-mapping.yaml as the single source of truth for all environment configuration, including feature flags. The generator reads from this unified schema and produces:

  • TypeScript interfaces (FeatureFlags, FeatureFlagsExternalConfig)
  • Default values (FEATURE_FLAG_DEFAULTS)
  • NgRx selectors for each flag
  • Config parser (parseFeatureFlags()) for runtime parsing
  • Updates to env.template.json for NGINX envsubst
  • Updates to Helm values.yaml for deployment configuration

Key benefits:

  • Adding a new feature flag requires only editing env-mapping.yaml and running the generator
  • Uses unified SYRF__ prefix for all environment variables (not legacy SPA__)
  • Same schema can define flags for .NET services (just change services: [] to include them)
┌─────────────────────────────────────────────────────────────┐
│           src/charts/syrf-common/env-mapping.yaml            │
│                  (Single Source of Truth)                    │
│                                                              │
│  featureFlags:                                               │
│    services: []  # Web only (or [api, pm] for .NET)          │
│    envVars:                                                  │
│      - name: SYRF__FeatureFlags__MyNewFeature                │
│        valuePath: featureFlags.myNewFeature                  │
│        default: "false"                                      │
│        web: { tsName: myNewFeature, tsType: boolean }        │
│        featureFlag: true                                     │
│        category: uiFeatures                                  │
│        description: "Enable my new feature"                  │
└─────────────────────────────────┬────────────────────────────┘
                        pnpm run generate:flags
           ┌──────────────────────┼──────────────────────┐
           ▼                      ▼                      ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│  TypeScript Files   │ │  env.template.json  │ │   Helm values.yaml  │
│                     │ │                     │ │                     │
│  - types.ts         │ │  + myNewFeature:    │ │  featureFlags:      │
│  - constants.ts     │ │    "${SYRF__...}"   │ │    myNewFeature:    │
│  - selectors.ts     │ │                     │ │      false          │
│  - parser.ts        │ │                     │ │                     │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘

File Locations

File Purpose
src/charts/syrf-common/env-mapping.yaml Schema definition (edit this!)
src/services/web/scripts/generate-feature-flags.ts Generator script
src/services/web/src/app/generated/feature-flags.types.ts Generated TypeScript interfaces (types only, no JS output)
src/services/web/src/app/generated/feature-flags.constants.ts Generated runtime constants (as const)
src/services/web/src/app/generated/feature-flags.selectors.ts Generated NgRx selectors
src/services/web/src/app/generated/feature-flags.parser.ts Generated config parser
src/services/web/src/assets/data/env.template.json Auto-updated envsubst template
src/services/web/.chart/values.yaml Auto-updated Helm values

Adding a New Feature Flag

Step 1: Edit the Schema

Add your flag to src/charts/syrf-common/env-mapping.yaml in the featureFlags section:

featureFlags:
  services: []  # Web only; add [api, project-management, quartz] for .NET services
  displayName: "Feature Flags"
  envVars:
    # ... existing flags ...

    - name: SYRF__FeatureFlags__MyNewFeature
      valuePath: featureFlags.myNewFeature
      default: "false"
      web:
        tsName: myNewFeature
        tsType: boolean
      featureFlag: true
      category: uiFeatures
      description: "Enable my new feature"

Step 2: Run the Generator

cd src/services/web
pnpm run generate:flags

Output:

Feature Flag Generator
======================
Schema: /home/.../feature-flags.schema.yaml

Loaded 31 feature flags

Categories:
  - Project Settings: 10 flags
  - Ui Features: 12 flags
  - Rob Features: 3 flags
  - Data Features: 1 flags
  - Development: 3 flags
  - Monitoring: 3 flags

Generated: src/app/generated/feature-flags.types.ts
Generated: src/app/generated/feature-flags.selectors.ts
Generated: src/app/generated/feature-flags.parser.ts

Updating env.template.json...
  + myNewFeature: "${SPA__MyNewFeature}"

Updating values.yaml...
  featureFlags section updated

Generation complete!

Step 3: Use the Flag in Components

Import and use the generated selector:

import { selectMyNewFeature } from '@app/generated/feature-flags.selectors';

@Component({
  selector: 'app-my-component',
  template: `
    @if (myNewFeatureEnabled()) {
      <div>New feature content!</div>
    }
  `
})
export class MyComponent {
  private store = inject(Store);

  // Using signals (recommended)
  myNewFeatureEnabled = this.store.selectSignal(selectMyNewFeature);

  // Or using observables
  myNewFeature$ = this.store.select(selectMyNewFeature);
}

Step 4: Configure for Deployment

The Helm values.yaml is auto-updated. To enable in a specific environment, update cluster-gitops:

# cluster-gitops/syrf/environments/staging/services/web.yaml
featureFlags:
  myNewFeature: true  # Enable in staging

Schema Properties Reference

Property Required Description
name Yes Environment variable name (SYRF__FeatureFlags__*)
valuePath Yes Path in Helm values.yaml (e.g., featureFlags.myNewFeature)
default Yes Default value as string ("true" or "false")
web.tsName Yes camelCase name used in TypeScript (e.g., myNewFeature)
web.tsType Yes TypeScript type (always boolean for feature flags)
featureFlag Yes Must be true to mark as feature flag
description Yes Human-readable description (used in JSDoc comments)
category Yes Grouping for organization (see categories below)
adminOnly No If true, flag requires admin UI to be visible

Categories

Use existing categories for consistency:

Category Purpose
projectSettings Project configuration features
uiFeatures General UI features
robFeatures Risk of Bias related features
dataFeatures Data import/export features
development Development/debugging features
monitoring Observability features (Sentry, APM, etc.)

Admin-Only Flags

Flags marked adminOnly: true are automatically gated by the admin UI state:

- name: SYRF__FeatureFlags__RobAiTestButton
  valuePath: featureFlags.robAiTestButton
  default: "false"
  web:
    tsName: robAiTestButton
    tsType: boolean
  featureFlag: true
  category: robFeatures
  description: "Show ROB AI test button (admin only)"
  adminOnly: true

Generated selector automatically combines with admin state:

export const selectRobAiTestButton = createSelector(
  selectConfigState,
  selectShowAdminUiOptions,
  (configState: AppConfig, showAdminUi: boolean): boolean =>
    configState.robAiTestButton && showAdminUi,
);

Generator Commands

cd src/services/web

# Generate all files
pnpm run generate:flags

# Validate schema without writing (CI/pre-commit friendly)
pnpm run validate:flags

# Preview changes without writing files
pnpm run generate:flags -- --dry-run

# Show verbose output including Helm env vars
pnpm run generate:flags -- --verbose

Pre-commit Hooks

Two pre-commit hooks validate generated code when relevant files change:

1. validate-env-blocks (syrf-common)

Triggered by changes to:

  • src/charts/syrf-common/env-mapping.yaml
  • src/charts/syrf-common/templates/_env-blocks.tpl

2. validate-feature-flags (web)

Triggered by changes to:

  • src/charts/syrf-common/env-mapping.yaml
  • src/services/web/src/app/generated/feature-flags.*.ts
  • src/services/web/src/assets/data/env.template.json
  • src/services/web/.chart/values.yaml

If validation fails (files are stale), the commit is blocked until you run the appropriate generator.

CI Validation

The validate-generated-code job in pr-tests.yml runs both validators:

  1. Detects if schema or generated files changed
  2. Runs pnpm run validate:env-blocks in syrf-common
  3. Runs pnpm run validate:flags in web
  4. Reports status in PR test summary

Why validate-only (not auto-generate)? Generated TypeScript could break the build. Developers should run the generator, review changes, test the build passes, then commit.

Note: The web chart's deployment.yaml env vars are generated by syrf-common/scripts/generate-env-blocks.ts, not the web generator. The web chart uses shared templates via {{ include "syrf-common.env.webConfig" . }} and {{ include "syrf-common.env.featureFlags" . }}.

The generators are idempotent - running them multiple times produces identical output. This ensures consistent validation in CI and pre-commit hooks.

How It Works at Runtime

  1. Build time: Helm templates reference values.yaml paths
  2. Container startup: NGINX envsubst replaces ${SPA__*} placeholders in env.template.json
  3. App bootstrap: main.ts fetches config JSON files
  4. Config parsing: parseFeatureFlags() converts string values to booleans
  5. State management: NgRx store holds parsed config
  6. Component access: Selectors provide typed access to flags
Helm values.yaml → deployment.yaml env vars
                   Docker container
           NGINX envsubst → appConfig.env.json
                main.ts fetch()
           parseFeatureFlags() → AppConfig
              NgRx Store (config state)
              selectMyFeature selector
               Component signal/observable

Architecture: AppConfigService Integration

The AppConfigService cleanly separates generated code (feature flags) from manual code (derived values):

// app-config.service.ts

// AppConfig = FeatureFlags (generated) + AppSettings (manual)
export interface AppConfig extends FeatureFlags, AppSettings {}

// ExternalConfig = FeatureFlagsExternalConfig (generated) + ExternalConfigSettings (manual)
export interface ExternalConfig extends FeatureFlagsExternalConfig, ExternalConfigSettings {}

static loadConfig(externalConfig: ExternalConfig): void {
  // Generated: parses all feature flags using schema defaults
  const featureFlags = parseFeatureFlags(externalConfig);

  // Manual: complex derived values (URLs, version strings, etc.)
  const appSettings = buildAppSettings(externalConfig);

  this.staticConfig = { ...featureFlags, ...appSettings };
}

Why Some Values Aren't Generated

Non-feature-flag configuration (AppSettings) requires manual logic that can't be generated:

Property Reason
displayVersion Environment-specific formatting (30+ lines of logic)
auth0Domain Computed from {tenant}.{region}.auth0.com
apiUrl Derived from apiOrigin + '/api'
protectedUrls String split + array append logic
randomId Runtime-generated browser session ID

File Structure

src/app/
├── generated/                      # AUTO-GENERATED (don't edit!)
│   ├── feature-flags.types.ts      # Interfaces only (no JS output)
│   ├── feature-flags.constants.ts  # FEATURE_FLAG_DEFAULTS, env vars, helm paths
│   ├── feature-flags.selectors.ts  # NgRx selectors
│   └── feature-flags.parser.ts     # parseFeatureFlags()
└── core/services/config/
    ├── app-config.service.ts       # Main service (uses generated code)
    └── app-settings.interface.ts   # Non-feature-flag types (manual)

Note: types.ts contains only TypeScript interfaces (erased at compile time). All runtime values (as const) are in constants.ts for clear separation.

Troubleshooting

Generated files out of sync

pnpm run generate:flags
git diff  # Review changes
pnpm run build  # Ensure TypeScript compiles

Validation fails in CI

The validate-generated-code job reports which files are stale:

# Run locally to see the same output
cd src/services/web
pnpm run validate:flags

This checks if env.template.json, values.yaml, and TypeScript files match the schema.

Pre-commit hook fails

# Run the generator to update files
pnpm run generate:flags

# Check what changed
git diff

# If TypeScript changed, test the build
pnpm run build

# Then stage the generated files
git add src/app/generated/ src/assets/data/env.template.json .chart/values.yaml

Flag not working in deployment

  1. Check Helm values in cluster-gitops
  2. Verify deployment has correct env var: kubectl exec ... -- env | grep SYRF__FeatureFlags
  3. Check browser dev tools → Network → fetch for config JSON
  4. Verify flag value in parsed config (browser console)

TypeScript build fails after generation

Generated code might reference types that don't exist yet. Check:

  1. Is selectConfigState exported from the config selectors?
  2. Is selectShowAdminUiOptions available for admin-only flags?
  3. Are NgRx imports correct in the generated selectors file?