Skip to content

Configuration

Valerio edited this page Apr 21, 2026 · 4 revisions

Configuration

Configuration is built on the standard Microsoft.Extensions.Configuration pipeline. Both the WebApp and the Worker use the same override chain. Later providers win:

appsettings.json                 typed defaults, checked in
  -> .env                         Docker/default developer stack values
  -> .env.local                   host-local overrides, only outside containers
  -> process environment          CI/CD, Docker Compose, shell variables
  -> Azure Key Vault              production secrets, optional
  -> HarpyxConfigurationComposer  derived composite keys, if still empty

The wiring happens in WebApplicationBuilderExtensions.ConfigureHarpyxHost and HostApplicationBuilderExtensions.ConfigureHarpyxHost. See src/Harpyx.WebApp/Extensions/WebApplicationBuilderExtensions.cs and src/Harpyx.Worker/Extensions/HostApplicationBuilderExtensions.cs.

Naming Convention

All layers target the same logical key path as appsettings.json:

Layer Separator Example
appsettings.json : Database:Host, EntraId:TenantId
Environment variables __ Database__Host, EntraId__TenantId
Azure Key Vault -- Database--Host, EntraId--TenantId

There are no ${...} placeholders in appsettings.json. Environment values directly override the same appsettings key through the native __ to : mapping performed by AddEnvironmentVariables(). Azure Key Vault uses the native -- to : mapping.

IConfiguration lookups are case-insensitive, but Harpyx standardizes on PascalCase environment names so they visually mirror the JSON structure.

Why -- and __?

  • Azure Key Vault secret names can contain dashes but not underscores, so the official convention maps : to --.
  • POSIX environment variables can contain underscores but not dashes, so .NET maps : to __.

Appsettings

appsettings.json contains structure and safe typed defaults. Environment-specific addresses and secrets are intentionally blank there and supplied by .env, .env.local, process environment variables, or Key Vault.

This keeps appsettings portable:

"Database": {
  "Host": "",
  "Port": 0,
  "Name": "",
  "Username": "",
  "Password": "",
  "TrustServerCertificate": true
}

The actual values live in environment-shaped files:

Database__Host=sqlserver
Database__Port=1433

Database:ApplyMigrationsOnStartup is intentionally omitted from appsettings.json. When omitted, the WebApp applies migrations automatically in Development and leaves them disabled in other environments. Production deployments should still set Database__ApplyMigrationsOnStartup=false explicitly and apply migrations out-of-band.

Seed Options

SeedOptions controls bootstrap data that must exist before an administrator can use the UI. Admin users are configured as an array under SeedOptions:AdminUsers:

"SeedOptions": {
  "AdminUsers": [
    {
      "Provider": "EntraId",
      "UniqueId": "8f8c9e0d-56af-4fef-aef6-0867b95900a3",
      "Email": "azure@ryadel.com"
    },
    {
      "Provider": "Google",
      "UniqueId": "google-sub-value",
      "Email": "admin@example.com"
    }
  ]
}

The environment-variable form uses normal .NET array binding:

SeedOptions__AdminUsers__0__Provider=EntraId
SeedOptions__AdminUsers__0__UniqueId=8f8c9e0d-56af-4fef-aef6-0867b95900a3
SeedOptions__AdminUsers__0__Email=azure@ryadel.com

SeedOptions__AdminUsers__1__Provider=Google
SeedOptions__AdminUsers__1__UniqueId=google-sub-value
SeedOptions__AdminUsers__1__Email=admin@example.com

Each admin entry has three fields:

Field Purpose
Provider Identity provider family. EntraId maps UniqueId to User.ObjectId; Google maps it to User.SubjectId.
UniqueId Stable provider-specific identifier. For Entra ID this is the object id; for Google this is the sub claim.
Email Human-readable address and fallback lookup key. It is not the primary identity anchor.

At WebApp startup, InitializeHarpyxDatabaseAndSeedAsync reads SeedOptions:AdminUsers, creates missing users, and ensures configured seed users are active platform admins. The database still stores provider-specific identifiers separately as ObjectId and SubjectId; UniqueId is only the provider-neutral configuration name.

Legacy SeedAdmin:ObjectId / SeedAdmin:Email values are still accepted as an Entra ID admin entry, but new configuration should use SeedOptions:AdminUsers.

Composite Values

Two keys are derived at startup from simpler components by HarpyxConfigurationComposer:

Composite key Derived from
ConnectionStrings:Harpyx Database:Host, Database:Port, Database:Name, Database:Username, Database:Password, Database:TrustServerCertificate
EntraId:Authority EntraId:Instance, EntraId:TenantId

HarpyxConfigurationComposer.ComposeDerivedValues runs after environment variables and Key Vault have been added, and writes a composite key only when that key is still empty. Operators can bypass composition by supplying a full value such as ConnectionStrings__Harpyx.

.env and .env.local

Copy .env.example to .env. This is the Docker/default developer stack configuration. Docker Compose reads it automatically, so docker compose up works without extra --env-file arguments.

Copy .env.local.example to .env.local when running the WebApp or Worker from the IDE / dotnet run while dependencies run in Docker. .env.local contains only host-local overrides, such as localhost endpoints and published ports:

# .env
COMPOSE_PROJECT_NAME=harpyx
DOTNET_ENVIRONMENT=Development
ASPNETCORE_URLS=http://+:8080

# .env.local
Database__Host=localhost
Database__Port=14330
Minio__Endpoint=localhost:9000
RabbitMQ__HostName=localhost
Redis__ConnectionString=localhost:6379
OpenSearch__Endpoint=https://localhost:9200
MalwareScan__Host=localhost

HarpyxEnvironmentFileLoader always loads .env first. When DOTNET_RUNNING_IN_CONTAINER is not true, it then loads .env.local as an optional overlay. Containers never load .env.local.

Internally, HarpyxEnvironmentFileLoader parses those files with DotNetEnv before AddEnvironmentVariables() runs. Values from the files enter the normal .NET environment-variable configuration path, so __ maps to : exactly like process environment variables.

Docker Compose

docker-compose.yml reads .env for interpolation and forwards the app-level variables into each service's environment: block. Docker-specific host ports and container knobs are also configured through .env using Docker__... variables:

COMPOSE_PROJECT_NAME=harpyx
DOTNET_ENVIRONMENT=Development
ASPNETCORE_URLS=http://+:8080

COMPOSE_PROJECT_NAME fixes the Docker Compose project name, so generated container, network, and volume names stay stable even if the repository folder is renamed. DOTNET_ENVIRONMENT selects the runtime environment for both WebApp and Worker, and ASPNETCORE_URLS binds Kestrel to the WebApp container port.

Docker__WebAppPort=8080
Docker__SqlServerPublicPort=14330
Docker__SqlServerContainerPort=1433

Container images sometimes require fixed uppercase variable names, such as MSSQL_SA_PASSWORD or OPENSEARCH_INITIAL_ADMIN_PASSWORD. Those names remain uppercase in docker-compose.yml because they are imposed by upstream images, but their values are still drawn from .env.

Azure Key Vault

To move production secrets to Azure Key Vault, add the provider after AddEnvironmentVariables() and before ComposeDerivedValues:

builder.Configuration.AddAzureKeyVault(
    new Uri(builder.Configuration["KeyVault:Uri"]!),
    new DefaultAzureCredential());

Store secrets using the -- separator:

Key Vault secret name Maps to config key
Database--Password Database:Password
EntraId--ClientSecret EntraId:ClientSecret
Minio--SecretKey Minio:SecretKey
Encryption--MasterKey Encryption:MasterKey

Because appsettings, env files, process variables, and Key Vault all target the same section path, application code is identical in every environment.

Key Sections

Section Purpose
Database SQL Server connection components and migration toggle
EntraId Microsoft Entra ID OIDC parameters
Google Google OAuth client id/secret/callback
SeedOptions Bootstrap data, including configured platform admin users
Minio Object storage endpoint and credentials
RabbitMQ Job queue broker
Redis Idempotency/cache connection string
OpenSearch Chunk index host, credentials, index name, and timeouts
Rag Chunking and embedding parameters
MalwareScan ClamAV host/port and fail-closed policy
UploadSecurity Allowed extensions and MIME types
ReverseProxy Forwarded headers configuration
DataProtection ASP.NET Data Protection keys location
Encryption AES-256-GCM master key for user LLM API keys
Email SMTP settings
OpenTelemetry OTLP exporter endpoint
Serilog Structured logging levels

Clone this wiki locally