# Flow F6 — Service startup + schema migration > One-shot per process start. Idempotent migrator (`CREATE ... IF NOT EXISTS`). Post-B9 the migrator additionally `DROP TABLE IF EXISTS orthophotos / gps_corrections` for fielded edge devices that previously ran the legacy schema. ## Description `Program.cs` builds the DI graph from environment, runs `DatabaseMigrator.Migrate(db)` once inside a startup scope, then starts serving HTTP. The migrator only owns the 4 tables this service is responsible for (`vehicles`, `missions`, `waypoints`, `map_objects`); the borrowed tables (`media`, `annotations`, `detection`) are migrated by their owner services in their own startups. ## Preconditions - `DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` all resolve via `ConfigurationResolver.ResolveRequiredOrThrow`. Any missing value aborts startup before the host is built — there are no hardcoded fallbacks. - `postgres-local` is reachable. - The `azaion` database itself exists in PostgreSQL (created at provisioning time, NOT by this migrator). - `admin` does NOT need to be reachable at process start. The JWKS fetch is lazy — it happens on the first protected request, not during the startup sequence diagrammed below. ## Sequence Diagram ```mermaid sequenceDiagram autonumber participant Docker as Docker / Watchtower participant Host as 07_host (Program.cs) participant Cfg as IConfiguration participant Identity as 05_identity participant DI as DI container participant Migrator as DatabaseMigrator participant DB as postgres-local Docker->>Host: ENTRYPOINT dotnet Azaion.Missions.dll Host->>Cfg: ResolveRequiredOrThrow DATABASE_URL → ConvertPostgresUrl → Npgsql connection string Host->>Cfg: ResolveRequiredOrThrow JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL — throw on any miss Host->>Identity: AddJwtAuth(builder.Configuration) — DI registration + ConfigurationManager wiring (no network yet) Host->>Cfg: read CorsConfig:AllowedOrigins, CorsConfig:AllowAnyOrigin Host->>Cfg: CorsConfigurationValidator.EnsureSafeForEnvironment — throws in Production when origins=[] AND allowAnyOrigin=false Host->>DI: register CORS policy (permissive OR WithOrigins(...)) Host->>DI: register controllers + middleware + scoped AppDataConnection + scoped services Host->>DI: build Host Host->>Migrator: scope.Resolve(); Migrate(db) Migrator->>DB: CREATE TABLE IF NOT EXISTS vehicles (...) Migrator->>DB: CREATE TABLE IF NOT EXISTS missions (...) Migrator->>DB: CREATE TABLE IF NOT EXISTS waypoints (...) Migrator->>DB: CREATE TABLE IF NOT EXISTS map_objects (...) Migrator->>DB: CREATE INDEX IF NOT EXISTS ix_missions_vehicle_id, ix_waypoints_mission_id, ix_map_objects_mission_id Migrator->>DB: DROP TABLE IF EXISTS orthophotos Migrator->>DB: DROP TABLE IF EXISTS gps_corrections note right of Migrator: B9 one-shot. Idempotent on devices that already cleaned up. Migrator-->>Host: void Host->>Host: emit PermissiveDefaultWarning if implicit-permissive CORS applies (non-Production with empty origins) Host->>Host: register ErrorHandlingMiddleware FIRST in pipeline Host->>Host: UseCors / UseAuthentication / UseAuthorization Host->>Host: MapControllers + MapGet("/health") + UseSwagger Host->>Docker: app.Run() — listening on 0.0.0.0:8080 ``` ## Flowchart ```mermaid flowchart TD Start([Container start]) --> ResolveDB[ResolveRequiredOrThrow DATABASE_URL] ResolveDB --> ResolveJwt["ResolveRequiredOrThrow JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL"] ResolveJwt --> CorsCfg[Read CorsConfig:AllowedOrigins + CorsConfig:AllowAnyOrigin] CorsCfg --> CorsGate{Production AND origins=[] AND allowAnyOrigin=false?} CorsGate -->|yes| FailFast([InvalidOperationException — Watchtower restarts]) CorsGate -->|no| RegDI[DI registrations: JWT bearer + JWKS manager, CORS, controllers, scoped DB + services] RegDI --> Build[Build Host] Build --> CorsWarn{Implicit-permissive CORS in this env?} CorsWarn -->|yes| LogWarn[Log PermissiveDefaultWarning] CorsWarn -->|no| OpenScope[Open startup scope] LogWarn --> OpenScope OpenScope --> Migrate[Run DatabaseMigrator.Migrate] Migrate --> Create["CREATE TABLE IF NOT EXISTS x4 + indexes"] Create --> Drop["DROP TABLE IF EXISTS orthophotos, gps_corrections (B9)"] Drop --> Pipeline[Wire pipeline: error MW first, auth, controllers, /health, Swagger] Pipeline --> Run([app.Run on :8080]) ResolveDB -. on missing value .-> FailFast ResolveJwt -. on missing value .-> FailFast Migrate -. on failure .-> Crash([Process exits non-zero — Watchtower restarts]) ``` ## Data Flow | Step | From | To | Data | Format | |------|------|----|------|--------| | 1 | Environment / IConfiguration | `Program.cs` (via `ConfigurationResolver`) | `DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` | string (required) | | 2 | Environment / IConfiguration | `Program.cs` | `CorsConfig:AllowedOrigins` (string[]), `CorsConfig:AllowAnyOrigin` (bool) | optional | | 3 | `Program.cs` | DI container | service registrations + JWT bearer + JWKS `ConfigurationManager` | C# code | | 4 | `DatabaseMigrator` | `postgres-local` | DDL statements (CREATE / INDEX / DROP) | SQL | | 5 | `Program.cs` | OS / Docker | bind to `0.0.0.0:8080` | TCP listener | ## Error Scenarios | Error | Where | Detection | Recovery | |-------|-------|-----------|----------| | Missing `DATABASE_URL` / `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` | `ConfigurationResolver.ResolveRequiredOrThrow` | env var + config key both empty/whitespace | Process exits non-zero with `InvalidOperationException` whose message names the missing env var and config key. Watchtower restarts but the new container hits the same failure. **Fix**: provide the value via env or `appsettings.json` | | CORS misconfigured in Production (`CorsConfig:AllowedOrigins=[]` AND `CorsConfig:AllowAnyOrigin!=true`) | `CorsConfigurationValidator.EnsureSafeForEnvironment` at startup | hard-fail guard | Process exits with `InvalidOperationException("CORS is misconfigured: ...")`. **Fix**: set `CorsConfig:AllowedOrigins` to the production UI origins, or set `CorsConfig:AllowAnyOrigin=true` to opt in explicitly | | `postgres-local` unreachable | Migrate step | Npgsql `IOException` / `SocketException` | Process exits non-zero. Watchtower restarts; `flight-gate` prevents restart mid-mission. **Fix**: ensure `postgres-local` healthcheck passes before `missions` starts (compose `depends_on` with `condition: service_healthy`) | | `azaion` database missing | Migrate step | Npgsql `3D000` (`invalid_catalog_name`) | Process exits. Operator must create the database — provisioning concern, not this service. Documented in `../../suite/_docs/00_top_level_architecture.md` | | `DROP TABLE IF EXISTS orthophotos` fails because table is locked by `gps-denied` | B9 one-shot | Lock timeout or `55006` | Process exits, Watchtower restarts in a few seconds. **Out-of-band ordering**: deploy `gps-denied` FIRST so it has its own copy of the schema before `missions` drops the legacy tables. Documented in B9 ticket | | One `CREATE TABLE` succeeds, the next fails | Mid-Migrate | Npgsql exception on later statement | Process exits. Each statement is individually idempotent (`IF NOT EXISTS`) so the next startup retries safely from the start. No partial-migration cleanup needed | | Wrong PostgreSQL version (e.g., < 13) | Migrate step | Specific syntax errors in newer features | Process exits. Suite-supported version is PG 16+; older devices need a Postgres upgrade | | `DATABASE_URL` malformed (e.g. user password contains `@`) | `ConvertPostgresUrl` | parse failure / silent mis-parse | `ConvertPostgresUrl` does NOT URL-decode user/password — caveat for credentials with `@`, `:`, `/`, `%`. `07_host` Caveats. Mitigation: avoid those chars in passwords, OR pass a raw Npgsql key=value string instead of a URL | ## Performance Expectations | Metric | Target | Notes | |--------|--------|-------| | Cold start total time | <2 seconds typical on Jetson Orin | Migrator runs ~10 DDL statements; all are no-ops on a steady-state device | | Cold start with legacy GPS-Denied tables present | +1 second | First-time-on-device B9 `DROP` adds two DDL statements | | Crash recovery (Watchtower restart) | ~10 seconds | Container restart latency dominates |