# Solution — Azaion.Missions
> **Status**: derived-from-code (autodev `/document` Step 5, 2026-05-14).
> **Mode**: retrospective synthesis from the verified `_docs/02_document/` set.
> **Forward-looking caveat**: this document describes the **post-rename, post-GPS-Denied-removal** target the documentation already reflects. Today's source still uses `Azaion.Flights.*`, `Aircraft*`/`Flight*`/`Orthophoto*`/`GpsCorrection*` filenames, and `[Route("aircrafts"|"flights")]`. The implementation gap is tracked under Jira AZ-EPIC (AZ-539) children B4–B12; the doc-vs-code reconciliation table lives in `_docs/02_document/04_verification_log.md` § 0. References to "the implemented solution" in this document mean the code as it exists today **plus** the deltas closed by B4–B12.
---
## 1. Product Solution Description
`missions` is the **edge-tier .NET 10 REST service** that owns the **mission domain** of an Azaion deployment — vehicle inventory (Plane / Copter / UGV / GuidedMissile), mission plans, ordered waypoints, and the cross-service cascade-delete that keeps the rest of the edge stack consistent when missions or waypoints are removed.
**Runtime topology**: exactly one container per device (Jetson Orin / OrangePI / operator-PC), co-located with `annotations`, the detection pipeline, `autopilot`, `gps-denied`, and the React `ui`. All edge services share **one local PostgreSQL** on the device; each migrates and writes only the tables it owns. JWTs are minted by the central `admin` service and validated locally with a shared HMAC secret — `missions` never calls back.
### Component interaction (high-level)
```mermaid
flowchart LR
ui[[Operator UI]]
admin[[admin service
JWT issuer]]
autopilot[[autopilot]]
annotations[[annotations]]
detection[[detection pipeline]]
gps[[gps-denied service]]
subgraph missions["missions (this service, one .NET process)"]
direction TB
h07[07_host
Program.cs / DI / startup]
c01[01_vehicle_catalog]
c02[02_mission_planning]
p04[04_persistence
AppDataConnection + Migrator]
i05[05_identity
JWT bearer + FL policy]
h06[06_http_conventions
error envelope + pagination]
end
pg[(postgres-local
shared per device)]
ui -- "REST + JWT" --> c01
ui -- "REST + JWT" --> c02
admin -. "shared HMAC secret (token only)" .-> i05
c01 --> p04
c02 --> p04
c02 -. "cross-service cascade delete" .-> annotations
c02 -. "cross-service cascade delete" .-> detection
autopilot <-- "DB read missions/waypoints
DB write map_objects" --> pg
p04 --> pg
annotations <--> pg
detection <--> pg
gps -. "no runtime coupling
(GUID refs only)" .-> pg
h07 --> c01
h07 --> c02
h07 --> p04
h07 --> i05
h07 --> h06
```
The HTTP surface is summarised in `_docs/02_document/system-flows.md` (7 flows, F1–F7); the per-component HTTP routes are listed in `_docs/02_document/components/0[1,2]_*/description.md`.
---
## 2. Architecture (as implemented)
The dominant pattern is **thin ASP.NET Core controller → service class → linq2db active-record over a per-request scoped `DataConnection`**, with **no repository abstraction** and **no in-process message queue / event bus**. ADR rationale (ADR-001 .. ADR-008) is in `_docs/02_document/architecture.md` § 8.
### 2.1 Per-component solution table
| # | Component | Solution (what it does) | Tools / libs | Advantages | Limitations | Requirements satisfied | Security | Cost | Fit |
|---|-----------|-------------------------|--------------|------------|-------------|------------------------|----------|------|-----|
| 01 | `01_vehicle_catalog` | Vehicle CRUD + `is_default` exclusivity. Controller `[Authorize(Policy="FL")]` → `VehicleService` → `ITable` | ASP.NET Core, linq2db `ITable`, `[Authorize]` | Single owner of the inventory abstraction; same exact pattern as `02_mission_planning` so engineers context-switch cheaply | "Exactly one default" is enforced by clear-then-set without a transaction → race window (B12 decision pending); no input validation on `Name`/`BatteryCapacity` (carry-forward) | Spec § 6.1 (Vehicle Catalog), suite roles `FL` | `[Authorize(Policy="FL")]` on every action; no per-method authz | One service file + one controller (~190 LoC together) | **Good** — matches operator-paced load, vertical scale only |
| 02 | `02_mission_planning` | Mission + Waypoint CRUD + the **cross-service cascade-delete walk**. Existence-checks `vehicle_id` on create/update; paginates `GET /missions` (the only paginated endpoint). | ASP.NET Core, linq2db, `PaginatedResponse` (`06_http_conventions`) | One canonical place that knows the full mission ownership graph; cascade walks `map_objects → media → annotations → detection → waypoints → missions` in FK order | **Cascade is NOT transaction-wrapped** (ADR-006) → partial failure leaves orphans; `UpdateWaypoint` is a full overwrite even though DTO looks partial; `vehicle_id` missing returns `400` (spec wants `404`); LinqToDB does not eager-load `[Association]` so `Vehicle` and `Waypoints` serialize null/empty | Spec § 6.2 (Mission Planning + Waypoints), spec § cascade contract | `[Authorize(Policy="FL")]` on every action; **no audit log**, no correlation id | Two service files + one controller (~370 LoC together); sequential I/O (4–7 round-trips per cascade) — single-digit ms typical against local Postgres | **Acceptable today; will need transaction wrap (one-line) before SLO commitments** |
| 04 | `04_persistence` | `AppDataConnection : DataConnection` exposes `ITable` for every persisted entity (4 owned post-B7+B9 + 3 borrowed read-only stubs). `DatabaseMigrator` runs `CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS` at startup; B9 adds a one-shot `DROP TABLE IF EXISTS orthophotos / gps_corrections` for fielded devices | linq2db 6.2.0, Npgsql 10.0.2, raw `Execute` for DDL | Lightweight; no migration tool dependency; idempotent every restart; `ITable` lets cross-component reads/cascades stay typed | No schema versioning; column drops / type changes need manual SQL or a future migration tool; no connection-pool tuning beyond Npgsql defaults | Spec § database schema, suite ER diagram (post-B7) | DB credentials are env-driven (`DATABASE_URL`); no column-level encryption; relies on PG-level access control | One file for the connection (~70 LoC) + one for the migrator (~120 LoC post-B9) | **Good for current schema scale (4 owned tables)**; will become limiting when schema starts evolving frequently |
| 05 | `05_identity` | `JwtExtensions.AddJwtAuth` registers `JwtBearer` with HMAC-SHA256 + the named policy `"FL"` (1-min clock skew). Validation is local; this service never calls `admin` | `Microsoft.AspNetCore.Authentication.JwtBearer` 10.0.5, `SymmetricSecurityKey` | `admin` outage does NOT take this service down (until tokens expire); zero-trip auth = lowest possible auth latency | `iss` / `aud` validation **disabled** (CMMC L2 row 3, AZ-487 / AZ-494 — suite-tracked, NOT in this Epic); the policy code `"FL"` retains the legacy "Flight" wording even after the service rename (fleet-wide auth change deferred); user-id claim is parsed but **not consumed** anywhere (no per-user audit) | Spec § auth, `../../suite/_docs/00_roles_permissions.md` | Shared HMAC secret (`JWT_SECRET`); rotation requires coordinated re-deploy across every backend that shares the secret | One file (~60 LoC) | **Good for the deployment shape (closed edge network behind a reverse proxy)**; `iss`/`aud` gap is a documented and tracked finding |
| 06 | `06_http_conventions` | `ErrorHandlingMiddleware` (global exception → JSON envelope) + `PaginatedResponse` + the **dead** `ErrorResponse` DTO | ASP.NET Core middleware, `System.Text.Json` (defaults) | Single chokepoint for HTTP wire shape — error mapping is uniform across components | **Two divergences from the suite spec carry forward** (ADR-002): entity/DTO bodies are PascalCase (no `JsonNamingPolicy.CamelCase`); error envelope misses spec's `errors` field. The error envelope IS already camelCase by accidental match (anonymous-object literal). The `ErrorResponse` DTO is dead on the wire and has the wrong shape (`List?` instead of spec's `object?` keyed by field name) | Spec § Error Response Format, § Pagination | `LogError(ex, ...)` only — no PII redaction (none in payload today); fallback `500` body shows the generic message, NOT the stack trace (logged only) | One middleware file + two DTO files (~80 LoC together) | **Acceptable until the suite-wide camelCase migration**; cutover is all-or-nothing because UI + autopilot consume PascalCase today |
| 07 | `07_host` | `Program.cs` composition root: env → connection string adapter, JWT registration, scoped DI for `AppDataConnection` + service classes, run migrator at startup, mount middleware in correct order, `MapGet("/health")`, mount Swagger | ASP.NET Core minimal host APIs | One file you can read top-to-bottom in one sitting; environment-fallback adapter (`ConvertPostgresUrl`) makes `dotnet run` zero-config in dev | **Swagger UI + dev fallbacks are NOT gated on `IsDevelopment()`** (ADR-005) — a misconfigured production deploy silently boots with `JWT_SECRET=development-secret-key-min-32-chars!!`; CORS is `AllowAnyOrigin/Method/Header` in every environment (assumed safe behind suite reverse proxy) | Spec § service composition; container `EXPOSE 8080`; Watchtower restart contract | Hardcoded dev fallbacks for `JWT_SECRET` / `DATABASE_URL` (security finding tracked at suite level) | One file (~150 LoC) | **Acceptable in the closed edge environment**; the unconditional Swagger + dev fallbacks are debt that should be paid when the service moves to a less trusted network |
### 2.2 Cross-cutting design choices
| Choice | Rationale | Status |
|--------|-----------|--------|
| One PostgreSQL per device, shared by all edge services (ADR-001) | 6× operational overhead saved per device; cross-service cascade is physically possible in one DB connection | **Implemented** |
| Manual cascade-delete in code, NOT `ON DELETE CASCADE` (ADR-003) | Schema-level cascade would couple `annotations` / detection schemas to this service's lifecycle | **Implemented** (transaction-wrap missing — ADR-006 carry-forward) |
| `CREATE TABLE IF NOT EXISTS` schema bootstrap (ADR-004), no migration tool | 4-table schema; no column drops or type changes; restart-driven deploy via Watchtower | **Implemented** (B9 adds the one explicit `DROP TABLE IF EXISTS` block for fielded devices) |
| Local JWT validation, no callback to `admin` (ADR-005, F5) | Zero auth-related coupling at runtime; `admin` outage doesn't take this service down | **Implemented** (`iss`/`aud` validation disabled — suite-tracked) |
| One csproj, one root namespace (ADR-008); layering by convention not by compiler | Service is small enough that 6 csprojs add more navigation cost than safety value | **Implemented** (post-B5); enforcement via `module-layout.md` § Allowed Dependencies + `/code-review` Phase 7 |
| GPS-Denied moved to a sibling service (ADR-007, B7+B9) | Different scaling + deployment cadence; GPS-Denied owns its tables and lifecycle | **Doc-only today**; B7 (code) + B9 (DB migration) close the gap |
### 2.3 Implementation order (relative to other components)
The 6 components have no circular dependencies. Implementation/refactor order (lowest layer first), per `_docs/02_document/module-layout.md`:
1. `04_persistence` (Layer 1) — depends only on linq2db + Npgsql.
2. `05_identity`, `06_http_conventions` (Layer 2) — depend on ASP.NET Core only.
3. `01_vehicle_catalog` (Layer 3) — depends on `04_persistence`, `05_identity`.
4. `02_mission_planning` (Layer 4) — depends on `01_vehicle_catalog` (vehicle existence check), `04_persistence`, `05_identity`, `06_http_conventions` (paginated envelope).
5. `07_host` (Layer 5) — depends on every other component (composition root).
Cross-component reads happen via the shared `AppDataConnection` (e.g. `02_mission_planning` reads `vehicles` to existence-check `vehicle_id`); the codebase does **not** wrap that lookup behind an interface in `01_vehicle_catalog`. This is intentional (one csproj, one DB) — see ADR-008.
---
## 3. Testing Strategy (as observed)
> **Status (today)**: **NO automated tests are present in this codebase.** The Tech Stack table in `_docs/02_document/architecture.md` § 2 says "Tests: None present"; `_docs/02_document/00_discovery.md` § Repository Layout confirms there is no `tests/` directory and the csproj has no test project sibling. The autodev `existing-code` flow's Phase A Steps 3 → 7 (Test Spec → Decompose Tests → Implement Tests → Run Tests) is the planned path to close this gap.
### 3.1 What exists today
| Layer | Coverage | Where it is |
|-------|----------|-------------|
| Unit | None | — |
| Integration / functional | None | — |
| Non-functional (perf, load, security) | None | — |
| Health probe | One endpoint (`GET /health` returns `{ status: "healthy" }`) | `Program.cs` `MapGet("/health")` |
| Schema sanity | Indirect — `DatabaseMigrator` runs at every startup; if a column/table is missing the process crashes | `Database/DatabaseMigrator.cs` |
| Wire-shape verification | Manual diff against `../../suite/_docs/00_top_level_architecture.md` § Error Response Format + § Pagination | Code review |
### 3.2 What the autodev `existing-code` flow will produce
- **Step 3 (Test Spec)** → `_docs/02_document/tests/traceability-matrix.md` + per-flow scenario files for F1–F7. The 8 ADRs and 7 carry-forward concerns from `architecture.md` are the seed set for test scenarios.
- **Step 4 (Code Testability Revision)** → minimal, surgical fixes if the codebase blocks tests from running (env-driven `DATABASE_URL` already lands here; hardcoded dev fallbacks in `Program.cs` are the prime candidate). Scope: smallest set of changes; deeper refactors deferred to Step 8.
- **Step 5 (Decompose Tests)** → per-test task files in `_docs/02_tasks/todo/`, plus `_test_infrastructure.md`.
- **Step 6 (Implement Tests)** → `tests/Azaion.Missions.Tests/` sibling project (xUnit is the suite-standard choice; per `coderule.mdc` "follow the established directory structure", no `src/` layer).
- **Step 7 (Run Tests)** → green test suite forms the safety net for Step 8 (Refactor) and every Phase B feature cycle thereafter.
### 3.3 Scenarios likely to land first (anticipated, not yet specified)
These are obvious test seams given the F1–F7 flows and the 7 carry-forward concerns; the actual scenario set is produced by Step 3.
| Priority | Scenario family | Why it lands first |
|----------|-----------------|--------------------|
| 1 | `MissionService.DeleteMission` — full cascade in dependency order | Critical-flow F3, NOT transaction-wrapped today; tests would catch any future regressions in the cascade chain immediately |
| 1 | `WaypointService.DeleteWaypoint` — scoped cascade variant | Same reason as F3; same NO-transaction caveat |
| 2 | `MissionService.CreateMission / UpdateMission` — `vehicle_id` existence check + spec-vs-code `400` vs `404` divergence | Locks in the current behaviour so the spec-conformance fix is intentional, not accidental |
| 2 | `VehicleService.SetDefault` / Create / Update — "exactly one default" race | B12 decision (spec-vs-code stricter behaviour) — tests pin whichever resolution the user picks |
| 2 | `ErrorHandlingMiddleware` mapping (`KeyNotFoundException → 404`, `ArgumentException → 400`, `InvalidOperationException → 409`, fallthrough → 500) | Wire-shape contract used by every flow |
| 3 | JWT validation — accept valid HS256 / reject invalid signature / reject expired (with 1-min skew) / reject missing-`FL` claim | F5 cross-cutting; pins the local-validation contract |
| 3 | `DatabaseMigrator.Migrate` — idempotent on a fresh DB, idempotent on already-migrated DB, B9 `DROP` on a fielded-legacy DB | F6; tests guard the only explicit destructive step |
---
## 4. Non-functional behaviour (observed)
| Concern | Observed behaviour | Where it is set | Notes |
|---------|--------------------|-----------------|-------|
| Latency | Single-digit ms typical; cascade delete = 4–7 sequential round-trips against local Postgres | `Database/AppDataConnection.cs` (per-request scope), `MissionService.DeleteMission` | No SLO in spec; observed under operator-paced load |
| Throughput | Operator-paced (~1 op/s peak); not load-tested | — | Edge deployment shape; not a hot path |
| Availability | Best-effort per device; Watchtower restarts on crash; `flight-gate` prevents restart mid-mission | `Dockerfile` + `../../suite/_infra/_compose/`, suite arch doc | No multi-instance HA per device by design |
| Recovery | RTO ≈ container restart time (~10s); RPO = device-local backup cadence (suite concern) | Watchtower + suite-level backup | — |
| Cascade atomicity | **Currently violated** (ADR-006); one-line fix queued | `Services/MissionService.cs`, `Services/WaypointService.cs` | Recommended to land with B6 |
| Wire-shape conformance | **Currently divergent** on entity/DTO case + error envelope's missing `errors` field (ADR-002) | `Program.cs` (no `JsonNamingPolicy.CamelCase`); `Middleware/ErrorHandlingMiddleware.cs` (anonymous object envelope) | Cutover is suite-wide; out of this Epic |
| Health endpoint | `< 10 ms` typical (no DB ping); used by Watchtower + reverse proxy | `Program.cs` `MapGet("/health")` | Future improvement: gate on DB ping |
| Resource limits | None in code; container-level limits set by edge compose | `Dockerfile` (no `--memory` / cpu limits inside) | Suite-level concern |
---
## 5. References
### 5.1 Source artefacts (this repo)
| Concern | File |
|---------|------|
| Web host composition | `Program.cs` |
| Vehicle catalog | `Controllers/AircraftsController.cs` (post-B6/B8: `Controllers/VehiclesController.cs`), `Services/AircraftService.cs` (post-B6: `VehicleService.cs`) |
| Mission planning | `Controllers/FlightsController.cs` (post-B6/B8: `Controllers/MissionsController.cs`), `Services/FlightService.cs` (post-B6: `MissionService.cs`), `Services/WaypointService.cs` |
| Persistence | `Database/AppDataConnection.cs`, `Database/DatabaseMigrator.cs`, `Database/Entities/*.cs` |
| Identity | `Auth/JwtExtensions.cs` |
| HTTP conventions | `Middleware/ErrorHandlingMiddleware.cs`, `DTOs/PaginatedResponse.cs`, `DTOs/ErrorResponse.cs` |
| Container | `Dockerfile` |
| CI | `.woodpecker/build-arm.yml` |
| Project | `Azaion.Flights.csproj` (post-B5: `Azaion.Missions.csproj`) |
### 5.2 Generated documentation (this repo)
| Doc | Path |
|-----|------|
| Discovery | `_docs/02_document/00_discovery.md` |
| Per-module docs (12 modules) | `_docs/02_document/modules/*.md` |
| Per-component descriptions (6 components) | `_docs/02_document/components/*/description.md` |
| Module layout (file ownership + layering) | `_docs/02_document/module-layout.md` |
| Architecture (this solution's source-of-truth) | `_docs/02_document/architecture.md` |
| System flows (F1–F7) | `_docs/02_document/system-flows.md` + `_docs/02_document/diagrams/flows/*.md` |
| Data model | `_docs/02_document/data_model.md` |
| Glossary (confirmed by user) | `_docs/02_document/glossary.md` |
| Verification log (drift mapping) | `_docs/02_document/04_verification_log.md` |
| Deployment notes | `_docs/02_document/deployment/{containerization,ci_cd_pipeline,environment_strategy,observability}.md` |
### 5.3 Suite-level cross-references
| Concern | File |
|---------|------|
| Primary spec for this service | `../../suite/_docs/02_missions.md` |
| Top-level architecture (error envelope, pagination, topology) | `../../suite/_docs/00_top_level_architecture.md` |
| Authoritative ER diagram (shared edge Postgres) | `../../suite/_docs/00_database_schema.md` |
| Roles & permissions (`FL` permission origin) | `../../suite/_docs/00_roles_permissions.md` |
| GPS-Denied (separate service after B7) | `../../suite/_docs/11_gps_denied.md` |
| CMMC L2 scorecard (JWT `iss`/`aud` finding) | `../../suite/_docs/05_security/cmmc_l2_scorecard.md` |
| Repo-config (post-rename) | `../../suite/_docs/_repo-config.yaml` |
### 5.4 Tracker (Jira AZ project)
| Plan ID | Jira | Type | SP | Status |
|---------|------|------|----|--------|
| Epic | [AZ-539](https://denyspopov.atlassian.net/browse/AZ-539) | Epic | — | To Do |
| B1 (local docs) | [AZ-540](https://denyspopov.atlassian.net/browse/AZ-540) | Task | 3 | **Done** |
| B2 (suite docs) | [AZ-541](https://denyspopov.atlassian.net/browse/AZ-541) | Task | 3 | **Done** |
| B3 (state bookkeeping) | [AZ-542](https://denyspopov.atlassian.net/browse/AZ-542) | Task | 3 | **Done** |
| B4 (repo rename) | [AZ-543](https://denyspopov.atlassian.net/browse/AZ-543) | Task | 3 | To Do |
| B5 (csproj + namespace) | [AZ-544](https://denyspopov.atlassian.net/browse/AZ-544) | Story | 3 | To Do |
| B6 (domain rename) | [AZ-545](https://denyspopov.atlassian.net/browse/AZ-545) | Story | 5 | To Do |
| B7 (drop GPS-Denied) | [AZ-546](https://denyspopov.atlassian.net/browse/AZ-546) | Story | 3 | To Do |
| B8 (HTTP routes) | [AZ-547](https://denyspopov.atlassian.net/browse/AZ-547) | Story | 3 | To Do |
| B9 (DB migration) | [AZ-548](https://denyspopov.atlassian.net/browse/AZ-548) | Story | 5 | To Do |
| B10 (Dockerfile + image tag) | [AZ-549](https://denyspopov.atlassian.net/browse/AZ-549) | Task | 2 | To Do |
| B11 (consumer cutover) | [AZ-550](https://denyspopov.atlassian.net/browse/AZ-550) | Story | 5 | To Do |
| B12 (default vehicle rule decision) | [AZ-551](https://denyspopov.atlassian.net/browse/AZ-551) | Task | 2 | To Do |
Leftover index: `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md`.