Environment Variable Code Generation¶
This document describes the Schema-Driven Configuration Code Generation system used in the SyRF monorepo. This system maintains a single source of truth for environment variables across all services, generating both Helm templates and TypeScript infrastructure.
Overview¶
The env-mapping system solves the DRY (Don't Repeat Yourself) problem for configuration:
- 143 environment variables defined once in
env-mapping.yaml - 25 configuration sections organised by functionality
- 4 services (api, project-management, quartz, web) with different subsets
- Two generators working from the same schema
Two-Generator Architecture¶
The system uses two separate generators from the same schema:
┌─────────────────────────────────────────────────────────────────────┐
│ env-mapping.yaml │
│ (Single Source of Truth) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Sections │ │ Services │ │ Web Config │ │
│ │ (25 total) │ │ Targeting │ │ (tsName, │ │
│ │ │ │ (api, pm, │ │ tsType) │ │
│ │ │ │ quartz) │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└────────────────────────────┬────────────────────────────────────────┘
│
┌───────────────────┴───────────────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ syrf-common/scripts │ │ web/scripts/ │
│ generate-env- │ │ generate-feature- │
│ blocks.ts │ │ flags.ts │
└─────────┬───────────┘ └─────────┬───────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ _env-blocks.tpl │ │ TypeScript Files │
│ (Helm Template) │ │ + JSON Config │
│ │ │ │
│ - syrf-common. │ │ - types.ts │
│ env.api │ │ - constants.ts │
│ - syrf-common. │ │ - selectors.ts │
│ env.pm │ │ - parser.ts │
│ - syrf-common. │ │ - env.template.json│
│ env.quartz │ │ - values.yaml │
│ - syrf-common. │ │ │
│ env.webConfig │ │ │
│ - syrf-common. │ │ │
│ env.featureFlags │ │ │
└─────────────────────┘ └─────────────────────┘
Generator 1: Helm Templates (syrf-common)¶
Generates: _env-blocks.tpl - Named Helm templates for all services
Used by: All service deployment.yaml files via {{ include "syrf-common.env.xxx" . }}
Scope: All environment variables (143 total across all services)
Generator 2: TypeScript + Config (web)¶
Generates: TypeScript interfaces, NgRx selectors, config parser, JSON templates
Used by: Angular application for type-safe feature flag access
Scope: Web configuration and feature flags (48 env vars)
File Locations¶
Syrf-Common Generator¶
| File | Purpose |
|---|---|
src/charts/syrf-common/env-mapping.yaml |
Schema definition (single source of truth) |
src/charts/syrf-common/scripts/generate-env-blocks.ts |
Helm template generator |
src/charts/syrf-common/templates/_env-blocks.tpl |
Generated Helm templates |
src/charts/syrf-common/package.json |
pnpm scripts for generation/validation |
Web Generator¶
| File | Purpose |
|---|---|
src/services/web/scripts/generate-feature-flags.ts |
TypeScript + config generator |
src/services/web/src/app/generated/feature-flags.types.ts |
Generated TypeScript interfaces |
src/services/web/src/app/generated/feature-flags.constants.ts |
Generated defaults and constants |
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 |
Generated envsubst template |
src/services/web/.chart/values.yaml |
Auto-updated Helm values |
Schema Structure (v2)¶
The env-mapping.yaml uses a section-based schema:
sectionName:
services: [api, project-management, quartz] # .NET services this applies to
displayName: "Human-Readable Name" # For comments in generated code
condition: valuePath.to.check # Optional: wrap in {{- if ... }}
envVars:
- name: SYRF__Section__Variable
# Value source (one of):
valuePath: helm.values.path # Direct from values.yaml
valueSource: # Dynamic Helm context
source: helm
path: .Release.Namespace
secretNamePath: path.to.secretName # Secret reference
secretKey: keyName # Static key
secretKeySource: # Dynamic key from Helm context
source: helm
path: .Chart.Name
configMapNamePath: path.to.configMap # Optional ConfigMap fallback
configMapKey: keyName
# Common properties
default: "default-value" # Default if value missing
transform: "pattern-{0}" # Value transformation
# Web infrastructure (triggers TypeScript generation)
web:
tsName: camelCaseName # TypeScript property name
tsType: boolean | string | number | string[]
# Feature flag infrastructure
featureFlag: true # Include in FeatureFlags interface
category: uiFeatures # Feature flag category
description: "Human description" # JSDoc comment
# Additional .NET services (additive to section services)
dotnet: [additional-service]
Section Properties¶
services (required)¶
Specifies which .NET services include this section's environment variables:
# All .NET services
services: [api, project-management, quartz]
# Only API and PM
services: [api, project-management]
# Only Quartz
services: [quartz]
# Web-only section (no .NET services)
services: []
condition (optional)¶
Wraps the entire section in a Helm conditional. Use for optional configuration blocks:
# Simple condition - checks if .Values.sentry exists
condition: sentry
# Nested condition - safely checks .Values.rabbitMq.queueNames
condition: rabbitMq.queueNames
Nested conditions generate safe traversal:
displayName (optional)¶
Human-readable name used in generated comments:
Environment Variable Properties¶
Value Sources¶
Each env var must have exactly one value source:
| Property | Description | Example |
|---|---|---|
valuePath |
Path in .Values |
mongoDb.clusterAddress |
valueSource |
Dynamic Helm context | { source: helm, path: .Release.Namespace } |
secretNamePath |
Secret name from values | auth0.clientSecretName |
Secret References¶
# Static secret key
- name: SYRF__Auth0__ClientSecret
secretNamePath: auth0.clientSecretName
secretKey: clientSecret
# Dynamic secret key from Helm context
- name: SYRF__Sentry__Dsn
secretNamePath: sentry.authSecretName
secretKeySource:
source: helm
path: .Chart.Name # Uses chart name as key (e.g., "api", "project-management")
ConfigMap Fallback¶
Supports optional ConfigMap with fallback to direct value:
- name: SYRF__DatabaseConfig__PSqlConfig__Hostname
valuePath: postgres.hostname
configMapNamePath: postgres.configMapName # If set, reads from ConfigMap
configMapKey: hostname
default: ""
Generated template:
{{- if .Values.postgres.configMapName }}
- name: SYRF__DatabaseConfig__PSqlConfig__Hostname
valueFrom:
configMapKeyRef:
name: {{ .Values.postgres.configMapName }}
key: hostname
{{- else }}
- name: SYRF__DatabaseConfig__PSqlConfig__Hostname
value: {{ .Values.postgres.hostname | default "" | quote }}
{{- end }}
Transform Patterns¶
Apply transformations to values:
# URL pattern transformation
- name: ASPNETCORE_URLS
valuePath: service.internalPort
default: "8080"
transform: "http://+:{0}"
# Result: http://+:8080
Web Infrastructure¶
Properties that trigger TypeScript generation:
- name: SYRF__FeatureFlags__SignalRActive
valuePath: featureFlags.signalRActive
default: "true"
web:
tsName: signalRActive # Property name in TypeScript
tsType: boolean # TypeScript type
featureFlag: true # Include in FeatureFlags interface
category: uiFeatures # Organizational category
description: "Enable SignalR real-time updates" # JSDoc comment
Available tsType values:
boolean- Parsed withJSON.parse()string- Used directlynumber- Parsed withparseFloat()string[]- Split by;delimiter
Generated Outputs¶
Helm Templates (_env-blocks.tpl) - Syrf-Common Generator¶
The syrf-common generator creates service-specific composite templates:
{{- define "syrf-common.env.api" -}}
# === GitVersion ===
{{ include "syrf-common.env.gitVersion" . }}
# === Runtime Environment ===
{{ include "syrf-common.env.runtime" . }}
# === MongoDB ===
{{ include "syrf-common.env.mongodb" . }}
...
{{- end -}}
Each section has its own template:
{{- define "syrf-common.env.sentry" -}}
{{- if .Values.sentry }}
- name: SYRF__CustomSentryConfig__Enabled
value: {{ .Values.sentry.enabled | default "false" | quote }}
- name: SYRF__Sentry__Dsn
valueFrom:
secretKeyRef:
name: {{ .Values.sentry.authSecretName }}
key: {{ .Chart.Name }}
{{- end }}
{{- end -}}
Web service uses these templates too:
# src/services/web/.chart/templates/deployment.yaml
env:
{{/* Web config and feature flags from syrf-common (generated from env-mapping.yaml) */}}
{{ include "syrf-common.env.webConfig" . | indent 10 }}
{{ include "syrf-common.env.featureFlags" . | indent 10 }}
TypeScript Files - Web Generator¶
The web generator (generate-feature-flags.ts) produces TypeScript files in src/app/generated/:
feature-flags.types.ts (interfaces)¶
export interface FeatureFlags {
/** Enable SignalR real-time updates */
signalRActive: boolean;
/** Enable Risk of Bias tool */
robToolEnabled: boolean;
// ... all 31 feature flags
}
export interface FeatureFlagsExternalConfig {
signalRActive?: string;
robToolEnabled?: string;
// ... (all strings from JSON)
}
feature-flags.constants.ts (defaults)¶
export const FEATURE_FLAG_DEFAULTS: FeatureFlags = {
signalRActive: true,
robToolEnabled: false,
// ...
} as const;
feature-flags.selectors.ts (NgRx)¶
export const selectSignalRActive = createSelector(
selectConfigState,
(config: AppConfig): boolean => config.signalRActive,
);
// Admin-only flags combine with admin UI state
export const selectRobAiTestButton = createSelector(
selectConfigState,
selectShowAdminUiOptions,
(config: AppConfig, showAdminUi: boolean): boolean =>
config.robAiTestButton && showAdminUi,
);
feature-flags.parser.ts (config parsing)¶
export function parseFeatureFlags(
external: Partial<FeatureFlagsExternalConfig>
): FeatureFlags {
return {
signalRActive: parseBoolean(external.signalRActive, true),
robToolEnabled: parseBoolean(external.robToolEnabled, false),
// ...
};
}
JSON Configuration Files - Web Generator¶
env.template.json¶
Used by NGINX envsubst at container startup:
{
"signalRActive": "${SYRF__FeatureFlags__SignalRActive}",
"apiOrigin": "${SYRF__ApiOrigin}",
"protectedUrls": "${SYRF__ProtectedUrls}"
}
Helm Values - Web Generator¶
The web generator also updates .chart/values.yaml:
Service Compositions¶
The generator automatically computes which sections each service needs:
| Service | Sections | Notes |
|---|---|---|
| api | 19 | Full backend service |
| project-management | 20 | Includes PM-specific queues |
| quartz | 17 | Scheduler with SQL connections |
| web | N/A | Uses TypeScript generation instead |
Running the Generators¶
Syrf-Common Generator (Helm Templates)¶
Output:
Environment Variable Block Generator v2
=======================================
Loaded 143 env vars in 25 sections
Service compositions:
api: 19 sections
project-management: 20 sections
quartz: 17 sections
Generated: templates/_env-blocks.tpl (867 lines)
Web Generator (TypeScript + Config)¶
Output:
Feature Flag Generator
======================
Schema: /path/to/env-mapping.yaml
Loaded 31 feature flags
Categories:
- Project Settings: 10 flags
- Ui Features: 12 flags
- ...
Generated: src/app/generated/feature-flags.types.ts
Generated: src/app/generated/feature-flags.constants.ts
Generated: src/app/generated/feature-flags.selectors.ts
Generated: src/app/generated/feature-flags.parser.ts
Updated: src/assets/data/env.template.json
Updated: .chart/values.yaml
Dry Run Mode¶
Preview changes without writing files:
# Syrf-common
cd src/charts/syrf-common
pnpm run generate:env-blocks -- --dry-run
# Web
cd src/services/web
pnpm run generate:flags -- --dry-run
Validation and CI¶
Pre-commit Hooks¶
Both generators have pre-commit hooks that validate generated code is up-to-date:
# .pre-commit-config.yaml
- id: validate-env-blocks
name: Validate Helm Env Blocks
entry: bash -c 'cd src/charts/syrf-common && pnpm run validate:env-blocks'
files: ^src/charts/syrf-common/(env-mapping\.yaml|templates/_env-blocks\.tpl)$
- id: validate-feature-flags
name: Validate Feature Flags
entry: bash -c 'cd src/services/web && pnpm run validate:flags'
files: ^(src/charts/syrf-common/env-mapping\.yaml|src/services/web/...)$
Why validate-only (not auto-generate)?
Generated TypeScript could break the build. Developers should:
- Run the generator manually
- Review changes
- Test that the build passes
- Commit the generated code
CI Validation¶
PR tests include a validate-generated-code job:
# .github/workflows/pr-tests.yml
validate-generated-code:
name: Validate Generated Code
if: needs.detect-changes.outputs.env_mapping_changed == 'true'
steps:
- name: Validate Helm env-blocks
run: pnpm run validate:env-blocks
working-directory: src/charts/syrf-common
- name: Validate feature flags
run: pnpm run validate:flags
working-directory: src/services/web
If validation fails, the PR shows which files are stale and need regeneration.
Validation Commands¶
# Validate Helm templates (without writing)
cd src/charts/syrf-common
pnpm run validate:env-blocks
# Validate web TypeScript (without writing)
cd src/services/web
pnpm run validate:flags
Conditional Rendering¶
Sections with condition property only render when the condition is truthy:
elasticsearch:
services: [api, project-management, quartz]
condition: elastic # Only render if .Values.elastic exists
This allows services to omit optional configuration. For example, Quartz doesn't define elastic in its values.yaml, so the elasticsearch env vars are skipped.
Nested Condition Handling¶
For nested paths like rabbitMq.queueNames, the generator creates safe traversal:
This prevents nil pointer errors when intermediate objects don't exist.
Best Practices¶
When to Add Conditions¶
Add condition when:
- The configuration is optional (not all services need it)
- The parent object might not exist in some values.yaml files
- The section uses nested paths that could be nil
Naming Conventions¶
| Convention | Example |
|---|---|
| Env var names | SYRF__Section__Property |
| Section names | camelCase matching values.yaml |
| TypeScript names | camelCase for properties |
| Secret paths | section.authSecretName or section.secretName |
Adding New Environment Variables¶
- Identify the appropriate section or create a new one
- Add the env var with proper value source
- Add
webobject if needed for web service - Add
featureFlag: trueif it's a feature toggle - Run the generator
- Test with
helm templateon affected services
Validation¶
The schema includes a JSON Schema for validation:
# Validate env-mapping.yaml against schema
pnpm exec ajv validate -s env-mapping.schema.json -d env-mapping.yaml
Integration with Helm Charts¶
Service charts include the generated templates via the syrf-common dependency:
# Chart.yaml
dependencies:
- name: syrf-common
version: "0.0.0"
repository: "file://../../../charts/syrf-common"
# templates/deployment.yaml
spec:
template:
spec:
containers:
- name: {{ .Chart.Name }}
env:
{{- include "syrf-common.env.api" . | nindent 12 }}
Related Documentation¶
- How-To: Manage Feature Flags - Adding feature flags (web generator)
- How-To: Extend Env-Mapping Schema - Adding env vars for .NET services
- Environment Variables Reference - Complete env var listing
- ADR-006: Helm Chart Standardization - Design decisions
Quick Reference¶
| Task | Generator | Command | Files Changed |
|---|---|---|---|
| Add feature flag | Web | pnpm run generate:flags |
TypeScript, env.template.json, values.yaml |
| Add .NET env var | Syrf-common | pnpm run generate:env-blocks |
_env-blocks.tpl |
| Add web config (non-flag) | Both | Run both generators | All above |
| Validate (pre-commit/CI) | Both | pnpm run validate:* |
None (read-only) |