# Configuration

> Every configuration key the AppointMe .NET 10 API reads, with its type, default, and where it's set locally (appsettings, Aspire) and on Azure (appsettings.Devtest.json, Key Vault references).

Source page: https://appointme.dev/docs/reference/configuration/

* **Files:** `appsettings.json` + `appsettings.<Env>.json`
* **Env var form:** `ConnectionStrings__AppointMeSql`
* **Azure secrets:** Key Vault references, set by Bicep
* **Validation:** Options are validated at startup

This page lists every key the API reads at commit `2ccbe87`, found by searching `src/` for options classes, `BindConfiguration`, `GetConnectionString` and `IConfiguration` reads. Each table gives the default from `appsettings.json` (or from code when the file doesn’t set it), the local value and the deployed `devtest` value. A dash means the key isn’t set there. Keys are alphabetical within each table.

## How configuration is loaded

**The standard ASP.NET Core order applies. Later sources win, and on Azure every secret arrives as an environment variable that App Service resolves from Key Vault.**

| Order | Source                               | Notes                                                                                       |
| ----- | ------------------------------------ | ------------------------------------------------------------------------------------------- |
| 1     | `src/AppointMe.Api/appsettings.json` | Defaults for every environment                                                              |
| 2     | `appsettings.<Environment>.json`     | `Development`, `Devtest` or `Codegen`                                                       |
| 3     | Environment variables                | `__` replaces `:`. Aspire, the Dockerfile and App Service app settings all arrive this way. |
| 4     | Command-line arguments               |                                                                                             |

The API project has no `UserSecretsId`, so user secrets aren’t loaded. On Azure, a Key Vault reference such as `@Microsoft.KeyVault(SecretUri=…)` is resolved by App Service before the API sees it, using the Web App’s managed identity.

| `ASPNETCORE_ENVIRONMENT` | Selected by                                                                                                    | What changes in code                                                                                                                                                            |
| ------------------------ | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Codegen`                | Code, when the entry assembly is `GetDocument.Insider` (build-time OpenAPI) or the arguments contain `codegen` | Placeholder values from `appsettings.Codegen.json`. Hangfire isn’t registered, migrations don’t run, and Wolverine runs as an in-memory mediator with the durability agent off. |
| `Development`            | `Properties/launchSettings.json`, so `dotnet run` and Aspire                                                   | `MapOpenApi()` is mapped, HSTS is off, and Wolverine generates handler code at runtime (`TypeLoadMode.Dynamic`)                                                                 |
| `Devtest`                | Bicep: `ASPNETCORE_ENVIRONMENT` app setting                                                                    | HSTS is on. Wolverine loads pre-generated handlers (`TypeLoadMode.Static`), which the Docker build writes with `codegen write`.                                                 |

Gotcha

Outside `Development`, Wolverine expects handler code generated ahead of time. If you run the API locally under another environment name with a plain `dotnet run`, nothing has generated that code. Use `Development`, or run `codegen write` first as the Dockerfile does.

## Connection strings

**One SQL database holds everything. The other two connection strings are optional.**

| Key                                       | Type   | Default         | Dev                                                                                                                                             | Devtest                                     | What it does                                                                                                                                                                                                                      |
| ----------------------------------------- | ------ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ConnectionStrings:AppointMeMessaging`    | string | `""`            | —                                                                                                                                               | —                                           | The Azure Service Bus connection. Read only when `Wolverine:Transport` is `AzureServiceBus`.                                                                                                                                      |
| `ConnectionStrings:AppointMeSql`          | string | None (required) | `appsettings.Development.json`: `Server=localhost,60740;Database=AppointMe;…`. Aspire injects its own for the `AppointMeSql` database resource. | Key Vault reference `AppointMeSql`          | Every module’s EF Core `DbContext` and Dapper reads, Wolverine’s message store (schema `wolverine`) and Hangfire storage (schema `hangfire`). When it’s missing, startup fails with `Connection string 'AppointMeSql' not found`. |
| `ConnectionStrings:DataProtectionStorage` | string | `""`            | —                                                                                                                                               | Key Vault reference `DataProtectionStorage` | An Azure Storage connection string. When it’s set, the Data Protection key ring is stored in blob `data-protection-keys/keys.xml`. When it’s empty, the default local key ring is used.                                           |

## Authentication and identity providers

**`Authentication:Provider` chooses Keycloak or Entra External ID. Only the chosen provider’s sections are bound, and every key in them is required and validated at startup.**

### Authentication

| Key                                   | Type                                    | Default                    | Dev                      | Devtest                  | What it does                                                                                                                                      |
| ------------------------------------- | --------------------------------------- | -------------------------- | ------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Authentication:Provider`             | string: `Keycloak` or `EntraExternalId` | `Keycloak` (file and code) | — (default)              | `EntraExternalId`        | Chooses the OIDC and JWT settings, the demo-login authenticator and the `IIdentityProvider`. Any other value throws at startup.                   |
| `Authentication:RequireHttpsMetadata` | bool                                    | `true` (code)              | `false`                  | `true`                   | Whether the OIDC and JWT handlers require HTTPS to fetch discovery metadata and keys. `Codegen` also sets `false`.                                |
| `Authentication:SuperAdmins`          | string\[]                               | `[]`                       | `["demo@appointme.dev"]` | `["demo@appointme.dev"]` | Emails, matched case-insensitively, that pass the `HangfireDashboard` policy for `/admin/jobs`. They also get `IsSuperAdmin` in `GET /api/v1/me`. |

### Authentication:Keycloak

Bound when `Authentication:Provider` is `Keycloak`. `KeycloakOptions` marks every property `[Required]`.

| Key                                            | Type   | Default | Dev                                                                                                               | Devtest | What it does                                                                             |
| ---------------------------------------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------- |
| `Authentication:Keycloak:ApiAudience`          | string | —       | `appointme-api`                                                                                                   | —       | The audience the JWT bearer handler expects                                              |
| `Authentication:Keycloak:Authority`            | string | —       | `https://localhost:8082/realms/appointme`. Aspire overrides it with Keycloak’s endpoint plus `/realms/appointme`. | —       | The OIDC authority. The demo login posts to `<Authority>/protocol/openid-connect/token`. |
| `Authentication:Keycloak:FrontendClientId`     | string | —       | `appointme-frontend`                                                                                              | —       | The OIDC client for browser sign-in and the demo password grant                          |
| `Authentication:Keycloak:FrontendClientSecret` | string | —       | A local-only value in `appsettings.Development.json`                                                              | —       | That client’s secret                                                                     |

### Authentication:EntraExternalId

Bound when `Authentication:Provider` is `EntraExternalId`. `EntraExternalIdOptions` marks every property `[Required]`, so the empty defaults fail validation until you fill them in.

| Key                                           | Type   | Default | Dev | Devtest                                           | What it does                                                                              |
| --------------------------------------------- | ------ | ------- | --- | ------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `Authentication:EntraExternalId:ApiAudience`  | string | `""`    | —   | `api://<client-id>`                               | The audience the JWT bearer handler expects                                               |
| `Authentication:EntraExternalId:Authority`    | string | `""`    | —   | `https://<tenant>.ciamlogin.com/<tenant-id>/v2.0` | The OIDC authority. The demo login derives the native-auth base `…/oauth2/v2.0/` from it. |
| `Authentication:EntraExternalId:ClientId`     | string | `""`    | —   | The app registration’s client ID                  | The OIDC client, and the client for native-auth demo login                                |
| `Authentication:EntraExternalId:ClientSecret` | string | `""`    | —   | Key Vault reference `EntraExternalIdClientSecret` | The OIDC client secret                                                                    |

### KeycloakAdmin

Bound by the Identity module when `Authentication:Provider` is `Keycloak`. `KeycloakAdminOptions` marks every property `[Required]`. It’s used to create users with the `VERIFY_EMAIL` and `UPDATE_PASSWORD` actions, and to resend the verification email.

| Key                          | Type   | Default | Dev                                                                     | Devtest | What it does                                                                        |
| ---------------------------- | ------ | ------- | ----------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------- |
| `KeycloakAdmin:BaseUrl`      | string | —       | `https://localhost:8082`. Aspire overrides it with Keycloak’s endpoint. | —       | The Keycloak base URL for the admin API                                             |
| `KeycloakAdmin:ClientId`     | string | —       | `appointme-api`                                                         | —       | The client for the client-credentials flow, also used for the verify-email redirect |
| `KeycloakAdmin:ClientSecret` | string | —       | A local-only value in `appsettings.Development.json`                    | —       | That client’s secret                                                                |
| `KeycloakAdmin:Realm`        | string | —       | `appointme`                                                             | —       | The realm users are created in                                                      |

### EntraIdentity

Bound by the Identity module when `Authentication:Provider` is `EntraExternalId`. `EntraIdentityOptions` marks every property `[Required]`. It builds a Microsoft Graph client with `ClientSecretCredential` that sends invitations and looks users up by email.

| Key                          | Type   | Default | Dev | Devtest                                                            | What it does                          |
| ---------------------------- | ------ | ------- | --- | ------------------------------------------------------------------ | ------------------------------------- |
| `EntraIdentity:ClientId`     | string | `""`    | —   | The same client ID as above                                        | The app that calls Graph              |
| `EntraIdentity:ClientSecret` | string | `""`    | —   | Key Vault reference `EntraExternalIdClientSecret`, the same secret | Its secret                            |
| `EntraIdentity:TenantId`     | string | `""`    | —   | The External ID tenant ID                                          | The tenant the credential signs in to |

[Entra External ID](/docs/deploy/entra-external-id/) shows where each value comes from in the portal.

## Demo and frontend

**`Demo:Enabled` turns on the public demo login, the demo-data seeding and two daily jobs. `Frontend:BaseUrl` is where every sign-in and invitation flow sends the user back.**

| Key                  | Type                   | Default                  | Dev                  | Devtest                     | What it does                                                                                                                                                                                                                                                    |
| -------------------- | ---------------------- | ------------------------ | -------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Demo:Enabled`       | bool                   | `false`                  | `true`               | `true`                      | Lets `GET /api/v1/login/demo` sign in (it returns `404` when off). `DemoSeedingSaga` seeds each new company with 150 customers, 3 appointments each. Registers the `crm:top-up-demo-customers` and `booking:top-up-demo-appointments` jobs, daily at 03:00 UTC. |
| `Demo:User:Email`    | string                 | —                        | `demo@appointme.dev` | `demo@appointme.dev`        | The demo user, which must already exist in the identity provider. If `Demo:User` is missing, the demo login returns `404`.                                                                                                                                      |
| `Demo:User:Name`     | string, optional       | —                        | `John Doe`           | `John Doe`                  | The display name used when the token has no name claim. Falls back to `Demo User`.                                                                                                                                                                              |
| `Demo:User:Password` | string                 | —                        | `AppointMe1`         | `AppointMe1`                | The demo user’s password, sent to Keycloak’s password grant or Entra’s native-auth API                                                                                                                                                                          |
| `Frontend:BaseUrl`   | absolute URI, required | `https://localhost:5173` | — (default)          | `https://app.appointme.dev` | Where users land after sign-in, sign-out and demo login. The invitation link is `<BaseUrl>/auth/login`. Validated with `[Required, Url]` at startup.                                                                                                            |

Turning `Demo:Enabled` off doesn’t remove recurring jobs that are already registered. [Production checklist](/docs/deploy/production-checklist/) covers that.

## Messaging, jobs, Data Protection and telemetry

**Wolverine has one key. Hangfire and Data Protection reuse connection strings, and OpenTelemetry reads only the standard `OTEL_*` variables.**

| Key                                     | Type                                      | Default                         | Dev                               | Devtest                                                               | What it does                                                                                                                                                                                                                |
| --------------------------------------- | ----------------------------------------- | ------------------------------- | --------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `APPLICATIONINSIGHTS_CONNECTION_STRING` | env var                                   | —                               | —                                 | Bicep: the App Insights connection string                             | Set on the Web App, but nothing in the API reads it                                                                                                                                                                         |
| `OTEL_EXPORTER_OTLP_ENDPOINT`           | env var                                   | The OTLP exporter’s own default | Aspire points it at its dashboard | Bicep: `https://<location>.in.applicationinsights.azure.com/v2/track` | Where the OTLP exporter sends traces and metrics                                                                                                                                                                            |
| `Wolverine:Transport`                   | string: `SqlDurable` or `AzureServiceBus` | `SqlDurable` (file and code)    | — (default)                       | `SqlDurable`                                                          | `SqlDurable` uses durable local queues on the SQL outbox. `AzureServiceBus` adds a Service Bus broker that auto-provisions its queues, and needs `ConnectionStrings:AppointMeMessaging`. Any other value throws at startup. |

These have no keys of their own:

| Feature         | Reads                                                   | Fixed in code                                                                                                                                                                                                  |
| --------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Data Protection | `ConnectionStrings:DataProtectionStorage`               | Application name `appointme`, container `data-protection-keys`, blob `keys.xml`                                                                                                                                |
| Hangfire        | `ConnectionStrings:AppointMeSql`                        | Schema `hangfire`, created if missing. Dashboard at `/admin/jobs`, gated by `Authentication:SuperAdmins`. Not registered in `Codegen`.                                                                         |
| OpenTelemetry   | `OTEL_*` environment variables                          | Service `AppointMe.Api`, version `1.0.0`. Traces: ASP.NET Core, HttpClient, SqlClient and `Wolverine`. Metrics: ASP.NET Core, HttpClient and `Wolverine*`. No logs.                                            |
| Wolverine       | `ConnectionStrings:AppointMeSql`, `Wolverine:Transport` | `MessageStorageSchemaName = "wolverine"` (the `wolverine_*` tables are still created in `dbo` at this commit), durable local queues, auto-applied transactions, `ValidationException` moved to the error queue |

## Host, container and App Service settings

**These are read by ASP.NET Core, the container or App Service itself, not by AppointMe code. You set them per environment, and several come from the Dockerfile or the Bicep.**

| Key                                              | Dev                                                                                                                                                                           | Devtest                            | What it does                                                                                                                       |
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `AllowedHosts`                                   | `*`                                                                                                                                                                           | `*`                                | ASP.NET Core host filtering. `*` accepts any `Host` header.                                                                        |
| `ASPNETCORE_ENVIRONMENT`                         | `Development` (launch profile)                                                                                                                                                | `Devtest` (Bicep)                  | Picks `appsettings.<Environment>.json` and the code paths in the first section                                                     |
| `ASPNETCORE_FORWARDEDHEADERS_ENABLED`            | —                                                                                                                                                                             | `true` (Bicep)                     | Turns on ASP.NET Core’s forwarded-headers handling for the App Service front end. `Program.cs` also calls `UseForwardedHeaders()`. |
| `ASPNETCORE_URLS`                                | The `https` launch profile, which Aspire uses: `https://localhost:7233;http://localhost:5179`. A plain `dotnet run` takes the first profile, `http`: `http://localhost:5179`. | `http://+:8080` (Dockerfile)       | Where Kestrel listens                                                                                                              |
| `DOCKER_REGISTRY_SERVER_URL`                     | —                                                                                                                                                                             | `https://<acr>.azurecr.io` (Bicep) | Read by App Service: the registry it pulls the image from                                                                          |
| `Logging:LogLevel:Default`                       | `Information`                                                                                                                                                                 | `Information`                      | Minimum log level                                                                                                                  |
| `Logging:LogLevel:Microsoft.AspNetCore`          | `Warning`                                                                                                                                                                     | `Warning`                          | Quietens framework request logs                                                                                                    |
| `Logging:LogLevel:Microsoft.EntityFrameworkCore` | —                                                                                                                                                                             | `Warning`                          | Set in `appsettings.Devtest.json` only                                                                                             |
| `WEBSITES_ENABLE_APP_SERVICE_STORAGE`            | —                                                                                                                                                                             | `false` (Bicep)                    | Read by App Service: no persistent `/home` mount in the container                                                                  |
| `WEBSITES_PORT`                                  | —                                                                                                                                                                             | `8080` (Bicep)                     | Read by App Service: the container port to route to                                                                                |

Build-time values that end up in the image or the SPA:

| Key                     | Where                                     | Default                  | What it does                                                                                                                                              |
| ----------------------- | ----------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `APP_VERSION`           | Docker build arg                          | `local`                  | Becomes `VITE_APP_VERSION`, the version in the SPA footer. CI passes the tag, or `main-<short sha>`.                                                      |
| `BUILD_CONFIGURATION`   | Docker build arg                          | `Release`                | The configuration for `codegen write` and `dotnet publish`                                                                                                |
| `VITE_API_PROXY_TARGET` | `src/AppointMe.Frontend/.env.development` | `https://localhost:7233` | Where the Vite dev server proxies `/api`, `/signin-oidc` and `/admin` outside Aspire. Under Aspire, `services__appointme-api__https__0` takes precedence. |

## Set in code, not configuration

**These look like settings but are constants. Change them in code if you need to.**

| Setting               | Value                                                                                                                      | Where                                                                                           |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Antiforgery           | Cookie `appointme.csrf`. The SPA reads the `XSRF-TOKEN` cookie and sends it back as `X-XSRF-TOKEN`.                        | `Antiforgery/`                                                                                  |
| Auth cookie           | `appointme.auth`: HttpOnly, SameSite Lax, always Secure, 7 days, sliding                                                   | `Authentication/AuthenticationExtensions.cs`                                                    |
| CSP                   | Sent as `Content-Security-Policy-Report-Only`                                                                              | `SecurityHeaders/SecurityHeadersMiddleware.cs`                                                  |
| Demo top-up schedule  | `0 3 * * *` (daily, 03:00 UTC)                                                                                             | The two `*JobRegistrar.cs` files                                                                |
| Forwarded host header | `X-Original-Host`, with every proxy trusted                                                                                | `Program.cs`                                                                                    |
| HSTS                  | 365 days, `IncludeSubDomains`, outside `Development`                                                                       | `Program.cs`                                                                                    |
| OIDC                  | Code flow, scopes `openid profile email`, callback `/signin-oidc`, pushed authorization disabled, only the `id_token` kept | `Authentication/AuthenticationExtensions.cs`                                                    |
| Startup migrations    | Every module’s `DbContext` migrates on startup, except in `Codegen`                                                        | `AppointMe.Shared/Database/Migrations`                                                          |
| Tenant header         | `X-Company-Id`                                                                                                             | `Program.cs`, see [Multi-tenancy](/docs/concepts/multi-tenancy/#how-a-request-gets-its-company) |

Paths in this table are relative to `src/AppointMe.Api/` unless they start with another project.

## Next

* [Deploy to Azure with Bicep](/docs/deploy/azure/) (Deploy)
* [Entra External ID](/docs/deploy/entra-external-id/) (Deploy)
* [Local services & ports](/docs/reference/local-services/) (Reference)
