Skip to content

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:

{{- if (and .Values.rabbitMq .Values.rabbitMq.queueNames) }}
...
{{- end }}

displayName (optional)

Human-readable name used in generated comments:

displayName: "RabbitMQ Queue Names"
# Generates: # === RabbitMQ Queue Names ===

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 with JSON.parse()
  • string - Used directly
  • number - Parsed with parseFloat()
  • 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:

featureFlags:
  signalRActive: true
  robToolEnabled: false
  # ... all feature flags with defaults

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)

cd src/charts/syrf-common
pnpm run generate:env-blocks

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)

cd src/services/web
pnpm run generate:flags

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:

  1. Run the generator manually
  2. Review changes
  3. Test that the build passes
  4. 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:

{{- if (and .Values.rabbitMq .Values.rabbitMq.queueNames) }}

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

  1. Identify the appropriate section or create a new one
  2. Add the env var with proper value source
  3. Add web object if needed for web service
  4. Add featureFlag: true if it's a feature toggle
  5. Run the generator
  6. Test with helm template on 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 }}

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)