---
title: Settings and Secrets
description: Manage application settings and Key Vault secrets across Forge environments.
moved_from:
  - guides/development/settings-and-secrets.md
---

# Settings and Secrets

This guide explains how to manage application settings and secrets in the Developer Platform.

## 📋 Overview

The platform uses a two-tier approach for configuration management:

- **Settings**: Non-sensitive configuration values. ASP.NET / API applications define these via `appsettings.{env}.json`; frontend/static web applications use `settings.yml`, wired directly into App Service environment variables by Terraform (no Azure App Configuration involved) — see [Settings Management](#settings-management) ⚙️
- **Secrets**: Sensitive values stored in Azure DevOps Libraries and deployed to Azure Key Vault 🔐

[TOC]

## ⚙️ Settings Management

This section covers two different app types with two different mechanisms — pick the one that matches what you're deploying:

- **ASP.NET / API applications** (e.g. generated from `saif-feature-api`, or any project with a real ASP.NET Core runtime): use `appsettings.{env}.json` — see [`appsettings.{env}.json`](#appsettingsenvjson) below.
- **Frontend / static web applications** (e.g. generated from `saif-feature-front-end-react` or `saif-feature-web-standalone` — static, Nginx-served SPAs with no .NET runtime): use `infra/web/settings.yml` — see [Frontend / Static Web Applications](#frontend-static-web-applications-settingsyml) below. This is **not** deprecated for this app type; it's the only mechanism available, since there's no ASP.NET Core process to load `appsettings.json`.

For ASP.NET / API applications, define non-sensitive settings in `appsettings.{env}.json` in the app project being deployed (not the AppHost). This is the standard ASP.NET Core configuration pattern and requires no separate pipeline step.

!!! warning "settings.yml is deprecated for ASP.NET / API applications"
    Some older API projects push settings via `settings.yml` (`infra/api/`) to Azure App Configuration. For ASP.NET / API applications, this mechanism still runs but is deprecated. Azure App Configuration can technically refresh registered settings at runtime (via `ConfigureRefresh(...).RegisterAll()` and `UseAzureDefaults()` in `SAIF.Platform.Azure`), but the platform doesn't rely on or encourage changing settings at runtime, so that capability isn't a reason to keep using it — it just adds a pipeline step and an extra config source over `appsettings.{env}.json`. Don't use it for new API settings; migrate existing `settings.yml` values to `appsettings.{env}.json` when you touch a project.

    This deprecation does **not** apply to frontend/static web applications — `infra/web/settings.yml` remains the correct, current mechanism for those. See [Frontend / Static Web Applications](#frontend-static-web-applications-settingsyml).

### `appsettings.{env}.json`

Define settings directly in the app project being deployed (the one that gets containerized), not in the AppHost. This follows standard ASP.NET Core layered configuration:

- `appsettings.json` — default values for all environments
- `appsettings.Development.json` — local development overrides (already present in generated templates)
- `appsettings.{env}.json` — per-environment overrides for deployed environments

ASP.NET Core selects the environment-specific file using `ASPNETCORE_ENVIRONMENT`, which the platform's Terraform sets to the environment's **lowercase short name** (`test`, `qa`, `uat`, `prod` — see `environment_short_name` in `saif-resources/modules/environment/local.tf`). Because deployed containers run on Linux (case-sensitive filesystem), name the files to match exactly:

```text
<project-root>/
├── src/
│   └── SAIF.App1/
│       ├── appsettings.json            # defaults
│       ├── appsettings.Development.json # local dev
│       ├── appsettings.test.json        # Test environment
│       ├── appsettings.qa.json          # QA environment
│       ├── appsettings.uat.json         # UAT environment
│       └── appsettings.prod.json        # Production
```

No pipeline configuration or App Configuration setup is required — the values ship inside the container image and ASP.NET Core loads the right file at startup based on the deployed environment.

### 🗄️ Legacy: `settings.yml` + Azure App Configuration (deprecated for API applications)

!!! note
    This section applies to ASP.NET / API applications only. If you're looking for frontend/static web app settings, see [Frontend / Static Web Applications](#frontend-static-web-applications-settingsyml) — the file format looks similar, but frontend apps' `settings.yml` values go straight into App Service environment variables via Terraform, never through Azure App Configuration, so that mechanism is not deprecated for that app type.

#### 📁Settings File Structure

Settings are defined in a `settings.yml` file located in the `infra/api/` folder of each project. The file uses a structured format that supports environment-specific overrides.

#### File Location

```text
<project-root>/
├── infra/
│   └── app/
│       └── settings.yml
```

#### Settings File Format

```yaml
# This is the settings.yml file used for configuration settings in the application.
# The settings are defined as a list of dictionaries, where each dictionary represents a setting.

# Example of a setting:
# - name: setting1          # The name of the setting
#   value: defaultvalue      # The default value of the setting
#   overrides:              # Optional overrides for specific environments
#     - env: test           # The environment where the override applies
#       value: testvalue    # The value of the setting for the specified environment

# Overrides allow you to specify different values for settings based on the environment.
# For example, you might have a default value for a setting that applies to all environments,
# but you can override this value for specific environments like 'test', 'qa', 'uat', or 'prod'.
# This is useful for managing environment-specific configurations without duplicating the entire settings structure.

- name: DatabaseConnectionTimeout
  value: '30'
  overrides:
    - env: test
      value: '10'
    - env: prod
      value: '60'

- name: ApiBaseUrl
  value: 'https://api.dev.example.com'
  overrides:
    - env: test
      value: 'https://api.test.example.com'
    - env: qa
      value: 'https://api.qa.example.com'
    - env: uat
      value: 'https://api.uat.example.com'
    - env: prod
      value: 'https://api.prod.example.com'

- name: LogLevel
  value: 'Debug'
  overrides:
    - env: prod
      value: 'Information'
```

#### Environment-Specific Overrides

The override system allows you to:

- Define a **default value** that applies to all environments
- Override specific values for targeted environments (`test`, `qa`, `uat`, `prod`)
- Avoid duplicating configuration across environments

### 🚀 Settings Deployment Process (settings.yml path, API applications)

1. **Development**: Define settings in `settings.yml` in your project
2. **Pipeline Processing**: The deployment pipeline reads the settings file
3. **Environment Resolution**: The pipeline applies environment-specific overrides
4. **Azure App Configuration**: Final settings are pushed to Azure App Configuration
5. **Application Access**: Applications retrieve settings from App Configuration

### 🌐 Frontend / Static Web Applications: `settings.yml`

Frontend/static web applications (e.g. generated from `saif-feature-front-end-react` or `saif-feature-web-standalone`) are static, Nginx-served SPAs with no ASP.NET Core runtime — they cannot load `appsettings.json`. For these apps, `settings.yml` is the correct, current mechanism, **not** a deprecated legacy path.

Unlike the API path above, frontend `settings.yml` values never pass through Azure App Configuration. The app's own Terraform (`web.generated.tf`) parses `infra/web/settings.yml` directly and wires the resolved values straight into App Service application settings (environment variables) — there's no `azurerm_app_configuration_key` resource in the frontend path at all.

#### File Location

```text
<project-root>/
├── infra/
│   └── web/
│       └── settings.yml
```

The file format is the same as the API `settings.yml` format above, but two things differ: the `name:` value must be prefixed with `APP_` (e.g. `APP_API_BASE_URL`) — see below for why — and environment-override matching is narrower (see [Environment-Specific Overrides](#environment-specific-overrides_1) below).

```yaml
- name: APP_API_BASE_URL
  value: 'https://api.dev.example.com'
  overrides:
    - env: test
      value: 'https://api.test.example.com'
    - env: prod
      value: 'https://api.prod.example.com'
```

#### Environment-Specific Overrides

Frontend override matching is narrower than the API path above: it compares each override's `env` (case-insensitive) against a *single* name — `environment_short_name` when the app defines one, otherwise `environment` — not both. The API path matches against either name. For example, with `environment = "Production"` and `environment_short_name = "prod"`, an override written as `env: Production` matches on the API side but is silently ignored on the frontend side (the frontend only matches `env: prod` here). Always write frontend overrides using the short environment name (`test`, `qa`, `uat`, `prod`) to avoid this mismatch.

#### How settings reach the running app

1. **Development**: Define settings in `infra/web/settings.yml` in your project, using an `APP_`-prefixed `name:` (e.g. `APP_API_BASE_URL`)
2. **Terraform Apply**: The app's Terraform (`web.generated.tf`) parses `settings.yml` and resolves environment-specific overrides at plan/apply time, then wires the resulting values in directly as App Service application settings (environment variables) on the container — no Azure App Configuration involved
3. **Container Startup**: An entrypoint script (`env.sh`, run via `/docker-entrypoint.d/`) reads only the environment variables whose names start with `APP_` (`env | grep -E '^APP_'`) and runs a `sed` substitution of each variable name against the static build output before Nginx starts serving it — a setting without the `APP_` prefix is present in the container's environment but is silently never substituted
4. **Application Access**: The static assets are served with the environment-specific values already baked in — there is no runtime configuration reload

Because step 3 does a `sed`-based text substitution rather than a templating pass, the frontend source must contain the literal placeholder text matching the env var name (e.g. `APP_API_BASE_URL`, not `{{ApiBaseUrl}}` or similar) wherever the value should be injected, so the `sed` replacement in `env.sh` can find it in the built output.

⚠️ This substitution is not a safe, exact find/replace of arbitrary text: `env.sh` runs `sed -i "s|${key}|${value}|g"`, inserting the value unescaped into both the `sed` command line and its replacement text. Avoid `|`, `&`, and backslash-digit sequences (e.g. `\1`) in frontend `settings.yml` values:

- A literal `|` in the value breaks the `sed` expression (it's the delimiter), causing `env.sh` to fail rather than substitute
- An unescaped `&` in the value is `sed`'s "insert the matched text" token, so it gets replaced with the matched `APP_...` key instead of your intended text — this silently corrupts the substituted value (e.g. a URL query string like `?a=1&b=2`)
- A backslash can alter or invalidate the `sed` expression beyond just backreferences (e.g. `\1`-style sequences, or a trailing backslash producing a malformed command)

These are the most common failure cases, not an exhaustive list of safe characters — `env.sh` also doesn't escape values for the destination file's format (JavaScript, JSON, HTML, CSS), so a value containing a quote, newline, or other syntax-sensitive character can produce invalid output even without using `|`, `&`, or `\`. Prefer simple values (URLs without query-string `&`, GUIDs, hostnames, plain strings) and verify the substituted output for anything less predictable.

Unlike the ASP.NET path above, this substitution happens once at container startup with no request-driven refresh at all, so there's no equivalent capability to weigh against `appsettings.{env}.json` — for frontend apps, `settings.yml` is simply the *only* available mechanism, so it stays current regardless.

## 🔐 Secrets Management

### 📚 Azure DevOps Library Setup

Secrets are managed through Azure DevOps Variable Groups (Libraries) with the following structure:

#### Create the Variable Group

A variable group is not created for you — create it once per project before your pipeline needs secrets:

1. In your Azure DevOps project, go to **Pipelines** → **Library**
2. Select **+ Variable group**
3. Set **Variable group name** to match the `SecretsVariableGroupName` you configure in your AppHost (`WithPipelineDefaults`); see [Aspire Publish](../deploy/aspire-publish.md#optional-configuration). The name is otherwise up to you; the project ID is a common, collision-free default.
4. Add your secrets (see [Secret Configuration Requirements](#secret-configuration-requirements)), marking each as **Secret**
5. Save, then grant pipeline access to the group if prompted (**Pipeline permissions** → allow your project's pipelines)

#### Library Naming Convention

- **Library Name**: Must exactly match the `SecretsVariableGroupName` value configured in your AppHost — the project ID is recommended if you have no other convention to follow
- **Example**: If your project ID is `it-api-exp-appname`, set `SecretsVariableGroupName = "it-api-exp-appname"` and name the library `it-api-exp-appname`

#### Pipeline Configuration

To pass the Azure DevOps Library to your pipeline templates, add the `secretsVariableGroupName` parameter to **both your main pipeline and PR pipeline**:

```yaml
extends:
  template: azure-dotnet-api-v3.yml@templates
  parameters:
    applicationName: ${{ variables.ApplicationName }}
    projectId: ${{ variables.ProjectId }}
    openAPIFileName: ${{ variables.OpenAPIFileName }}
    dotNetVersion: '10.x'
    secretsVariableGroupName: 'ProjectId1' # 🔑 Azure DevOps Library name
```

**Key Points:**

- The `secretsVariableGroupName` parameter tells the pipeline template which Azure DevOps Library contains your secrets
- **Add this parameter to both pipelines**: Configure it in your main deployment pipeline (`azure-pipelines-api.yml`) and your PR validation pipeline (`azure-pipelines-api-pr.yml`)
- It is recommended that the value matches your Project ID and the variable group name defined in the `variables` section of your pipeline
- Example: If your project ID is `it-api-exp-appname`, use `secretsVariableGroupName: 'it-api-exp-appname'`

#### Secret Naming Convention

```text
[ENV]<settingname>
```

Where:

- **ENV**: Environment prefix (`[GLOBAL]`, `[TEST]`, `[QA]`, `[UAT]`, `[PROD]`)
- **settingname**: The name of the secret setting

**Environment Prefix Options:**

- **`[GLOBAL]`**: Applied to all environments by default (can be overridden)
- **`[TEST]`**: Test environment specific
- **`[QA]`**: QA environment specific
- **`[UAT]`**: UAT environment specific
- **`[PROD]`**: Production environment specific

**Override Behavior:**
Environment-specific secrets take precedence over global secrets. For example:

- `[GLOBAL]DatabasePassword` applies to all environments
- `[PROD]DatabasePassword` overrides the global setting for production only
- Other environments (TEST, QA, UAT) continue using the global value

#### Examples

```text
# Global secrets (applied to all environments)
[GLOBAL]DatabasePassword
[GLOBAL]ApiKey
[GLOBAL]ServiceUrl

# Environment-specific secrets (override global if present)
[TEST]DatabasePassword      # Overrides global for TEST
[QA]DatabasePassword        # Overrides global for QA
[UAT]DatabasePassword       # Overrides global for UAT
[PROD]DatabasePassword      # Overrides global for PROD

# Mixed example - ApiKey uses global, DatabasePassword has env-specific overrides
[GLOBAL]ApiKey             # Used by all environments
[GLOBAL]DatabasePassword   # Used by QA and UAT
[TEST]DatabasePassword     # Overrides global for TEST
[PROD]DatabasePassword     # Overrides global for PROD
```

### Secret Configuration Requirements

For each secret in the Azure DevOps Library:

1. **Variable Type**: Must be set to `Secret` (not `Variable`)
2. **Environment Prefix**: Use uppercase environment names (`[GLOBAL]`, `[TEST]`, `[QA]`, `[UAT]`, `[PROD]`)
3. **Consistent Naming**: Use the same base name across all environments
4. **Override Hierarchy**: Environment-specific secrets override global secrets for that environment

**Secret Resolution Order:**

1. Check for environment-specific secret (e.g., `[PROD]DatabasePassword`)
2. If not found, use global secret (e.g., `[GLOBAL]DatabasePassword`)
3. If neither exists, the secret is not available

### 🚀 Secrets Deployment Process

1. **Azure DevOps Library**: Define secrets with proper naming convention
2. **Variable Type**: Ensure all secrets are marked as `Secret` type
3. **Pipeline Detection**: Deployment pipeline automatically detects secrets
4. **Key Vault Storage**: Secrets are securely stored in Azure Key Vault
5. **App Configuration Reference**: App Configuration receives references to Key Vault secrets
6. **Application Access**: Applications access secrets through App Configuration with Key Vault integration

## ✨ Best Practices

### ⚙️ Settings Best Practices

- **Use `appsettings.{env}.json` for ASP.NET / API apps**: `settings.yml` + App Configuration is deprecated for this app type — don't use it for new API settings. (Frontend/static web apps are a different case — see [Frontend / Static Web Applications](#frontend-static-web-applications-settingsyml), where `settings.yml` is still current and doesn't use App Configuration at all.)
- **Use Descriptive Names**: Choose clear, self-explanatory setting names
- **Default Values**: Always provide sensible default values in `appsettings.json`
- **Environment Consistency**: Maintain consistent setting names across environments
- **Match environment file casing**: Name `appsettings.{env}.json` files with the lowercase short name (`test`, `qa`, `uat`, `prod`) to match `ASPNETCORE_ENVIRONMENT` on Linux containers
- **Validation**: Test settings in lower environments before production

### 🔐 Secrets Best Practices

- **Minimal Exposure**: Only store truly sensitive data as secrets
- **Rotation**: Regularly rotate secrets, especially API keys and passwords
- **Access Control**: Limit access to Azure DevOps Libraries containing secrets
- **Audit Trail**: Monitor access to secrets in Azure Key Vault
- **Naming Convention**: Strictly follow the `[ENV]<settingname>` format

### 🛡️ Security Considerations

1. **Secrets Never in Code**: Never commit secrets to source control
2. **Environment Separation**: Ensure complete separation between environment secrets
3. **Least Privilege**: Grant minimum necessary permissions for accessing secrets
4. **Regular Review**: Periodically review and cleanup unused secrets
5. **Monitoring**: Enable monitoring and alerting for secret access

## 🔍 Troubleshooting

### ⚠️ Common Issues

#### Settings Not Appearing in App Configuration (settings.yml path, API applications)

- ✅ Verify `settings.yml` exists in `infra/api/` folder
- ✅ Check YAML syntax is valid
- ✅ Ensure pipeline has permissions to App Configuration
- ✅ Confirm environment name matches expected values

#### Frontend Settings Not Applying (settings.yml path, frontend/static web applications)

- ✅ Verify `settings.yml` exists in `infra/web/` folder and YAML syntax is valid
- ✅ Confirm the setting's `name:` is prefixed with `APP_` (e.g. `APP_API_BASE_URL`) — `env.sh` only picks up environment variables matching `^APP_`, so an unprefixed name is silently never substituted
- ✅ Confirm environment name in overrides matches expected values
- ✅ Check the App Service application settings (environment variables) in the Azure portal to confirm Terraform wired the value through — this path doesn't use Azure App Configuration, so App Config checks don't apply here
- ✅ Verify `env.sh` ran during container startup and that the static build output contains the literal `APP_...` placeholder string matching the environment variable name
- ✅ If the substituted value looks corrupted or `env.sh` failed outright, check whether the setting's value contains `|`, an unescaped `&`, a backslash, or characters that need escaping in the destination file's format (quotes, newlines) — `env.sh` passes the value unescaped into `sed -i "s|${key}|${value}|g"` with no destination-format escaping, so these can break the substitution, corrupt it, or produce invalid output

#### `appsettings.{env}.json` values not applying in a deployed environment

- ✅ Confirm the file name matches `ASPNETCORE_ENVIRONMENT` exactly, including case (Linux containers are case-sensitive) — e.g. `appsettings.test.json`, not `appsettings.Test.json`
- ✅ Verify the file is included in the build output / container image
- ✅ Check for a higher-precedence source (environment variables, App Configuration) overriding the value

#### Secrets Not Accessible

- ✅ Verify Azure DevOps Library name matches what is specified in `secretsVariableGroupName`
- ✅ Check secret naming follows `[ENV]<settingname>` format
- ✅ Ensure variable type is set to `Secret`
- ✅ Confirm pipeline has access to the variable group
- ✅ Verify Key Vault permissions are correctly configured

#### Environment-Specific Values Not Working

- ✅ Check environment name spelling in overrides
- ✅ Verify deployment pipeline is using correct environment parameter
- ✅ Ensure override structure follows the correct YAML format

### 💬 Getting Help

If you encounter issues with settings or secrets configuration:

1. **Check Pipeline Logs**: Review deployment pipeline output for errors
2. **Validate Configuration**: Use YAML validation tools for settings files
3. **Verify Permissions**: Ensure proper access to Azure resources
4. **Contact Platform Team**: Reach out for assistance with complex configurations

## 📂 Example Project Structure

The following shows an ASP.NET / API project structure; frontend/static web app projects instead have `infra/web/settings.yml` and no `appsettings.*.json` (see [Frontend / Static Web Applications](#frontend-static-web-applications-settingsyml)):

```text
AppnameApplication/
├── src/
│   └── SAIF.App1/
│       ├── appsettings.json          # Default settings (all environments)
│       ├── appsettings.Development.json
│       ├── appsettings.test.json
│       ├── appsettings.qa.json
│       ├── appsettings.uat.json
│       └── appsettings.prod.json
├── infra/
│   ├── app/
│   │   └── settings.yml          # Legacy, deprecated for API apps — only needed for the App Configuration alternative
│   └── [infrastructure code]
└── azure-pipelines.yml           # Pipeline configuration
```

Azure DevOps Library: `it-api-exp-appname` (matching project ID)

```text
# Global secrets (used by all environments unless overridden)
[GLOBAL]ApiKey = [secret]
[GLOBAL]ServiceUrl = [secret]

# Environment-specific secrets (override global when present)
[TEST]DatabasePassword = [secret]
[QA]DatabasePassword = [secret]
[UAT]DatabasePassword = [secret]
[PROD]DatabasePassword = [secret]

# Mixed example: EmailApiKey uses global for all environments
[GLOBAL]EmailApiKey = [secret]
```

This approach ensures secure, manageable, and environment-appropriate configuration for all applications in the Developer Platform.

## 💻 Using These Settings in Applications

### 🔵 .NET Applications

For detailed information on how to consume these settings in your .NET applications, see the official Microsoft documentation: [Configuration in .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration)

This guide covers:

- How to read configuration values in your application
- Working with Azure App Configuration provider
- Integrating with dependency injection
- Best practices for configuration management in .NET

### 🌐 Frontend / Static Web Applications

Frontend/static web apps don't read configuration at runtime the way ASP.NET apps do — there's no process to inject values into. Instead, values from `infra/web/settings.yml` are wired directly into the App Service as environment variables by the app's Terraform (no Azure App Configuration involved), and then consumed at container startup by an entrypoint script (`env.sh`) that substitutes them into the static build output before Nginx starts serving it. This only works for settings whose `name:` is prefixed with `APP_` (e.g. `APP_API_BASE_URL`), and the frontend source must contain that same string as a literal placeholder — `env.sh` runs a `sed` substitution, not templated substitution. This `sed` substitution is not a safe find/replace for arbitrary text: values containing `|`, an unescaped `&`, or a backslash can break or corrupt it, and values aren't escaped for the destination file's format either. Avoid those characters in frontend `settings.yml` values and verify the substituted output. See [Frontend / Static Web Applications](#frontend-static-web-applications-settingsyml) above for the full flow, including the specific unsafe characters.

---

## 📚 Related Documentation

- [Aspire Config Example](../examples/aspire-config.md) - Working example of publish-time configuration generation with Aspire
