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.jsonfor NGINX envsubst - Updates to Helm
values.yamlfor deployment configuration
Key benefits:
- Adding a new feature flag requires only editing
env-mapping.yamland running the generator - Uses unified
SYRF__prefix for all environment variables (not legacySPA__) - 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¶
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.yamlsrc/charts/syrf-common/templates/_env-blocks.tpl
2. validate-feature-flags (web)
Triggered by changes to:
src/charts/syrf-common/env-mapping.yamlsrc/services/web/src/app/generated/feature-flags.*.tssrc/services/web/src/assets/data/env.template.jsonsrc/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:
- Detects if schema or generated files changed
- Runs
pnpm run validate:env-blocksin syrf-common - Runs
pnpm run validate:flagsin web - 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¶
- Build time: Helm templates reference
values.yamlpaths - Container startup: NGINX envsubst replaces
${SPA__*}placeholders inenv.template.json - App bootstrap:
main.tsfetches config JSON files - Config parsing:
parseFeatureFlags()converts string values to booleans - State management: NgRx store holds parsed config
- 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¶
Validation fails in CI¶
The validate-generated-code job reports which files are stale:
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¶
- Check Helm values in cluster-gitops
- Verify deployment has correct env var:
kubectl exec ... -- env | grep SYRF__FeatureFlags - Check browser dev tools → Network → fetch for config JSON
- Verify flag value in parsed config (browser console)
TypeScript build fails after generation¶
Generated code might reference types that don't exist yet. Check:
- Is
selectConfigStateexported from the config selectors? - Is
selectShowAdminUiOptionsavailable for admin-only flags? - Are NgRx imports correct in the generated selectors file?
Related Documentation¶
- Environment Variable Code Generation - The broader env-mapping system
- Environment Variables Reference - Complete env var listing
- How-To: Extend Env-Mapping Schema - For non-feature-flag config