# 07 — Host (Composition Root) **Spec source**: `../../../suite/_docs/00_top_level_architecture.md` § Edge compose excerpt — confirms the port (`5002:8080`) and DB target (`postgres-local`). **The env-var contract in suite docs still references the legacy `JWT_SECRET`** and predates this service's transition to JWKS-based JWT validation; the four-variable env contract documented below is the verified current state in code, and the suite docs are flagged for sync in `_docs/02_document/05_drift_findings_2026-05-14.md`. **Implementation status**: ✅ implemented. > **NOTE (forward-looking)**: post-rename. Today's source has `Azaion.Flights` namespace + `dotnet Azaion.Flights.dll` entrypoint + container image `azaion/flights:*-arm`. Renames + DLL/image/compose changes tracked under Jira AZ-EPIC children B5 (namespace), B10 (Dockerfile + Woodpecker + suite compose). **Files**: `Program.cs`, `GlobalUsings.cs`, `Infrastructure/ConfigurationResolver.cs`, `Infrastructure/CorsConfigurationValidator.cs` ## 1. High-Level Overview **Purpose**: Build the ASP.NET Core web host: read environment, register all DI services, configure the request pipeline, run the schema migrator at startup, and serve the API on port 8080 (mapped to host 5002 in edge compose). **Architectural pattern**: Composition root + ASP.NET Core minimal-host bootstrap (top-level statements). **Upstream dependencies**: Every other component in this service. **Downstream consumers**: The container runtime (`ENTRYPOINT ["dotnet", "Azaion.Missions.dll"]` in `Dockerfile` after B10) and any local `dotnet run`. ## 2. Internal Interface None. The host has no exported types -- its surface is the running HTTP server. ## 3. External API | Endpoint | Method | Auth | Description | |----------|--------|------|-------------| | `/health` | GET | Public | Returns `{ "status": "healthy" }` | | `/swagger/*` | GET | Public | Swagger UI + JSON spec, served unconditionally in all environments | | (mapped controllers from feature components) | various | Per-controller `[Authorize]` | See components 01 (vehicles) and 02 (missions). | ## 4. Data Access Patterns - Opens a single scope at startup to call `DatabaseMigrator.Migrate(db)` -- populates the 4 owned tables in the shared local PostgreSQL. - Registers `AppDataConnection` as **scoped** so each HTTP request gets a fresh `DataConnection` (one Npgsql connection per request from the pool). ## 5. Implementation Details **State Management**: Stateless (request pipeline). The only run-once side effect is the migrator call. **Key Dependencies**: | Library | Version | Purpose | |---------|---------|---------| | `Microsoft.AspNetCore` (in `Microsoft.NET.Sdk.Web`) | net10.0 | Web host + middleware pipeline | | `linq2db` | 6.2.0 | DB access via `AppDataConnection` registration | | `Npgsql` | 10.0.2 | PostgreSQL driver (used through linq2db) | | `Swashbuckle.AspNetCore` | 10.1.5 | Swagger UI + JSON spec generation | **Error Handling**: Delegated to `06_http_conventions`' middleware, placed FIRST in the pipeline so it wraps everything else. **Configuration**: All required values flow through `Infrastructure/ConfigurationResolver.cs` → `ResolveRequiredOrThrow`. Resolution order per value: `Environment.GetEnvironmentVariable(envVar)` → `IConfiguration[configKey]` → throws `InvalidOperationException` at startup with a message naming both the env var and the config key. There are **no hardcoded dev fallbacks**; a misconfigured production deploy cannot silently boot. | Env var | Config key | Required? | Purpose | |---------|------------|-----------|---------| | `DATABASE_URL` | `Database:Url` | **Yes** | Either Npgsql key=value form OR a `postgresql://` URI (converted via `ConvertPostgresUrl`) | | `JWT_ISSUER` | `Jwt:Issuer` | **Yes** | Expected `iss` claim value (see `05_identity`) | | `JWT_AUDIENCE` | `Jwt:Audience` | **Yes** | Expected `aud` claim value (see `05_identity`) | | `JWT_JWKS_URL` | `Jwt:JwksUrl` | **Yes** | HTTPS URL of admin's JWKS endpoint (see `05_identity`) | | `CorsConfig:AllowedOrigins` | (same) | No (defaults to `[]`) | String array of allowed origins for the CORS policy | | `CorsConfig:AllowAnyOrigin` | (same) | No (defaults to `false`) | When `true`, applies `AllowAnyOrigin/Method/Header` regardless of origins | The legacy `JWT_SECRET` env var is **no longer consulted**; `05_identity` documents the JWKS-based replacement. **CORS gating** (`Infrastructure/CorsConfigurationValidator.cs`): - `EnsureSafeForEnvironment(origins, allowAnyOrigin, environmentName)` THROWS `InvalidOperationException` in `Production` (case-insensitive match on the `ASPNETCORE_ENVIRONMENT` value) when origins are empty AND `allowAnyOrigin` is `false`. The host refuses to start with an implicit permissive policy in Production. - `ShouldUsePermissivePolicy(origins, allowAnyOrigin)` returns `true` when `allowAnyOrigin == true` OR origins is empty — used by the CORS policy builder. In non-Production environments with empty origins this falls back to permissive. - `ShouldWarnAboutPermissiveDefault(origins, allowAnyOrigin)` is `true` when origins are empty AND `allowAnyOrigin` is `false` (implicit permissive). When true, the host logs `PermissiveDefaultWarning` at startup with the current environment name. **`ConvertPostgresUrl` helper**: ad-hoc parser converting `postgresql://user[:pass]@host[:port]/db` to Npgsql key=value form. Does not URL-decode user/password — caveat for credentials with `@`, `:`, `/`, `%`. ## 6. Extensions and Helpers - `GlobalUsings.cs` -- three project-wide `global using` directives for LinqToDB. ## 7. Caveats & Edge Cases - **Swagger is unconditional**: Swagger UI + JSON spec are mounted regardless of environment (no `IsDevelopment()` guard). This is the **only remaining** aspect of ADR-005 that still applies — the legacy "dev-fallback secret" aspect of ADR-005 is now obsolete (`ConfigurationResolver.ResolveRequiredOrThrow` throws on any missing value at startup). - **CORS hard-fail is `Production`-only**. In `Staging` or any custom environment name that is not literal `Production` (case-insensitive), an empty allow-list with `AllowAnyOrigin=false` falls back to permissive (with a startup warning) instead of throwing. Operators deploying to a "Staging" tier that should be locked down need to set `CorsConfig:AllowedOrigins` explicitly — the validator will not enforce it for them. - **JWKS startup dependency**: the first protected request after process start triggers a synchronous HTTPS fetch to `JWT_JWKS_URL`. If `admin` is unreachable at that moment, the request fails 500 from `05_identity`'s `IssuerSigningKeyResolver`. Once cached, request-path validation does not call `admin`. - **Migrator failure crashes the process** at startup. Container orchestrator (Watchtower-restarted Docker) is expected to bring it back; `flight-gate` (per `../../../suite/_docs/00_top_level_architecture.md`) ensures this doesn't happen mid-mission. - **No HTTPS redirection** middleware; assumes a TLS-terminating reverse proxy upstream (Caddy fronting Gitea is documented but in-deployment TLS termination is environment-specific). Note: `JWT_JWKS_URL` is independently constrained to HTTPS by `HttpDocumentRetriever { RequireHttps = true }` inside `05_identity`. - **Port 8080** matches the Dockerfile `EXPOSE 8080` and edge compose `5002:8080` mapping per `../../../suite/_docs/00_top_level_architecture.md` excerpt. - **No GPS-Denied service registration** here. Earlier drafts of this doc reserved a slot for a GPS-Denied feature component; per Jira AZ-EPIC child B7, GPS-Denied lives in a separate (out-of-this-repo) service, so this host registers only `VehicleService`, `MissionService`, `WaypointService`. ## 8. Dependency Graph **Must be implemented after**: every other component (01-06). **Blocks**: nothing internal (it is the runtime root). ## 9. Logging Strategy ASP.NET Core defaults (Console / Debug providers, no Serilog/structured logging configured). The only structured log emitted by app code is `06_http_conventions`' middleware `LogError(ex, "Unhandled exception")`. No correlation ID, no request tracing.