πŸͺžπŸ›‘️ FLYsafe.live - v2 - Cloud Custodian, Autopilot Runner & dTwin Strategy

The Custodian maintains the dTwin and independently verifies DAA execution; the Autopilot Runner is the protocol-agnostic edge that streams telemetry in and flies RTM Core's risk-checked maneuvers out — MAVLink and DJI alike
Version 2.0 · Supersedes the Custodian/Runner model in Aircraft Custodian & dTwin Strategy v1.13 with the DAA architecture from FLYsafe Architecture β€” DAA, Custodian & Runner Strategy v1.3
Prepared for Lindsay Mohr, AIRmarket · TECH-002 / FSL RTM Platform · Grounded against the am-flyrtm-blender source (Flight Blender / InterUSS derivative)

🎯 Core Thesis

One Cloud Custodian per drone, per tenant. The Custodian has two jobs, not one: it maintains the dTwin — the canonical row in tenant.drones that answers “what does FLYsafe.live believe is true about this drone right now?” — and it independently verifies, via the aircraft's real telemetry, that the Autopilot Runner executed what RTM Core commanded.

RTM Core decides. The Runner executes. The Custodian records and verifies. No component performs another's job: RTM Core never talks to the aircraft directly, the Runner never re-derives whether a conflict exists, and the Custodian never relays a command or picks a maneuver — it maintains the state of record and independently checks that execution matched intent.

Naming note: this document folds two prior strategies into one model. The Aircraft Custodian (formerly Aircraft Agent) is renamed Cloud Custodian here to match its counterpart, the Autopilot Runner — the protocol-specific edge component that used to be described separately as a “GCS Plugin” (telemetry-in) and “Custodian SW” (state-maintenance). The Runner now covers both directions: it streams telemetry into the Custodian and executes the Custodian-verified, RTM Core–computed avoidance maneuvers back onto the airframe. One edge component, one name, per protocol (MAVLink Runner, DJI Runner).

🎯 What changed from v1.13 (Aircraft Custodian & dTwin Strategy)

Contents

  1. System architecture β€” where & how drone data comes in
  2. The Custodian's four obligations
  3. The Autopilot Runner β€” telemetry in, avoidance out
  4. State lives in tenant.drones
  5. Cloud Custodian APIs
  6. Identity model β€” four-tier layout
  7. FLYplan workflow
  8. Surveillance & ownership tagging
  9. DAA execution model β€” RTM Core, maneuver selection & protocol behavior
  10. Lifecycle & operational model
  11. Open questions & next steps
  12. Reference β€” Polygon Generation API requirements

🎯 Solution Concept

Three components, one contract each. RTM Core is the cloud brain: flight binding, the fleet-wide picture, flight authorization, flight tracking, and all DAA reasoning. It publishes avoidance commands and proximity alerts once, and never talks to an aircraft directly. The Cloud Custodian is a per-aircraft supervisory layer: it maintains the dTwin in tenant.drones from telemetry the Runner forwards, and it independently verifies — from that same telemetry — that the Runner did what RTM Core said. The Autopilot Runner is the protocol-specific edge: it turns RTM Core's maneuver into native MAVLink or DJI commands, confirms execution from real telemetry (not just a command acknowledgment), and is the same component that streamed telemetry in in the first place.

MAVLink Aircraft
ArduPilot · PX4 · Sentaeros
DJI Aircraft
Matrice · Mavic Enterprise
↕ telemetry in / commands out
MAVLink Runner
embedded in Mission Planner desktop GCS
DJI Runner
Chrome GCS Plugin / DJI Cloud API
↓ normalized telemetry
RTTP β€” Real Time Telemetry Processing
ingest · validate · normalise · route (encompasses Flink)
↓
Cloud Custodian APIs
writes the dTwin · verifies Runner execution
↓ maintains
tenant.drones β€” the dTwin
Fig. 1 β€” Telemetry path: Runner (per protocol) β†’ RTTP β†’ Custodian APIs β†’ tenant.drones. The same Runner also executes RTM Core's avoidance commands (Fig. 2) and reports execution telemetry back through this same path.
RTM Core
avoidance commands · proximity alerts · fences · coverage
↓ publish once
AMQP Fanout / Topic Exchange (per flight declaration)
NOT a single competing-consumer queue
↓ copy of every message
Custodian's own bound queue
↓
Cloud Custodian
independent watchdog β€” never a relay
↓ copy of every message
Runner's own bound queue
↓
Autopilot Runner
acts immediately β€” no wait on Custodian
↓
Executes maneuver / fence upload on aircraft
Runner's real telemetry and status flow back to the Custodian, which compares “what the message said” against “what actually happened,” writes daa_status / current_avoidance_state to tenant.drones, and reports tiered DAA status (full / degraded / no qualified surveillance) to RTM Core.
Fig. 2 β€” Custodian and Runner as parallel, independent AMQP consumers off a shared exchange. See Β§3.7.

The five pillars of the v2.0 design

Custodian maintains the dTwinOne per drone. Honest representation of the aircraft's current state in tenant.drones — that is still its first job.
Runner is the unified edgeOne Runner per protocol (MAVLink, DJI). It streams telemetry in and executes RTM Core's avoidance maneuvers out — the same component, both directions.
RTM decides, Runner executes, Custodian verifiesNo component performs another's job. The Custodian is a TCAS-analogous watchdog, not a relay or a decision-maker.
State lives in PostgresThe dTwin is a row in tenant.drones. DAA status and avoidance state are just more columns on that same row.
APIs are the contractFrontend, Runner, MCP agents, and DAA-status consumers all read and write through the same Custodian API surface.

1. System Architecture — Where & How Drone Data Comes In

This is the structural view carried forward unchanged from v1.13: where drone records originate, how they enter tenant.drones, and where identity propagates.

πŸ“š FLYsafe.software DLB.Drones
inventory source of truth β€” brand, model, serial, TC registration, core RID fields
↓ Import process (Steve Smith) β€” one-way sync, DEV & PROD
drones.dlb_drones β€” raw DLB staging table
verbatim column names; never touched by RTM
↓ FK dlb_guid; seeds two speed defaults on first insert
drones.drones + v_dtwin β€” the dTwin
RTM-managed config · live state · DAA status · joined with dlb_drones
↓ identity sync
πŸ” Keycloak.ΞΈ.Drone
auth only β€” ID format <tenant>-<serial>
↓ cross-tenant publish
🌐 AADMS.drones
callsign β†’ (tenant, serial) routing for inbound ADS-B
↓ read & write via API
πŸ—ΊοΈ FSL RTM Map / OCC
frontend β€” same APIs as the Runner and MCP
Fig. 3 β€” Drone data setup flow, unchanged from Aircraft Custodian & dTwin Strategy v1.13 Β§1.

The Custodian maintains its drone's row in tenant.drones — that is its primary job. Telemetry enters via the Autopilot Runner (Β§3), gets normalized, and the Custodian writes state through the API. Keycloak.ΞΈ.Drone registers each drone as a user (<tenant>-<serial>) purely for authentication — no drone metadata lives there. AADMS resolves inbound ADS-B callsigns to (tenant, serial) for cross-tenant routing.

2. The Custodian's Four Obligations

An Cloud Custodian is the entity inside FLYsafe.live RTM responsible for representing one specific drone. There is exactly one Custodian per drone per tenant. In v1.13 it carried three obligations; v2.0 adds a fourth, sourced directly from the DAA strategy's parallel-AMQP-consumer redesign (Β§3.7).

πŸͺž Representation

Single point of drone identity in RTM. The canonical row in tenant.drones.

Observable via: GET /drones/{serial}; row presence.

πŸ“‘ Streaming & Source Vigilance

Honest telemetry flow with explicit failure declaration, forwarded by the Runner. See Β§3.6.

Observable via: data_source_status, adapter_health.connected.

🎯 Currency

Maintains canonical drone state — position, attitude, battery, mission binding, ADS-B status, OI defaults.

Observable via: timestamps.source_ts, updated_at.

πŸ›‘οΈ Verification New in v2.0

Independently confirms, via the aircraft's real telemetry, that the Runner executed RTM Core's avoidance command. Reports tiered DAA status back to RTM Core — never relays the command itself.

Observable via: daa_status, current_avoidance_state, last_daa_verification_at.

Why “Custodian” and not “Agent”. The term Custodian emphasises that the entity is responsible for the drone's representation in RTM — not merely a passive translation layer, and not a decision-maker. A Custodian has obligations it can succeed or fail at, observably. Verification is the clearest expression of that: the Custodian doesn't decide what maneuver to fly and doesn't execute one — it watches, and it tells the truth about what happened.

3. The Autopilot Runner — Telemetry In, Avoidance Out

The data path into the Cloud Custodian is a Runner embedded inside the operator's GCS — not a separate edge container the operator has to install and manage. The same Runner also executes RTM Core's avoidance commands and confirms execution from real telemetry. Two Runner form factors cover the bulk of the fleet:

🧩 DJI Runner (FlightHub 2, DJI Cloud API)

Host: Chrome GCS Plugin (browser extension) or direct DJI Cloud API · Protocol: GCS web APIs / DRC channel

Reads telemetry from the GCS's exposed web APIs and forwards to the Custodian API. On an avoidance command, issues descent-and-hold over the DRC live-control MQTT channel and watches control-status flags to confirm execution.

Extensibility. The Runner pattern is open-ended: any GCS that has a way to surface telemetry to a hosted environment (desktop SDK, browser hooks, or local REST) can have a Runner built for it. DroneSense, Skydio Cloud, and other vendor GCSs are candidates as fleets grow. The Custodian API contract stays the same; only the Runner's data-extraction and command-translation code differs.

3.5 Centralized Configuration Management via Custodian APIs

The Cloud Custodian doesn't just maintain telemetry-derived state — it's also the system of record for how each aircraft connects. The protocol-specific configuration for every supported telemetry source (MAVLink autopilot, DJI FlightHub 2, DJI Cloud API, ADS-B) is stored in tenant.drones and exposed through the Custodian APIs.

How configuration flows

The Custodian calls the APIs to obtain its own configuration at bootGET /drones/{serial}/config returns the full configuration document including which protocol adapter to load and its connection parameters. The Runner doesn't carry hardcoded protocol details; everything is data, served from tenant.drones.

The Custodian provides protocol configuration through the API surface — operators (via the RTM Map UI) and AI agents (via MCP) read and update these configurations through the same APIs. One central place to manage every aircraft's connection, regardless of vendor.

DJI FlightHub 2

For: DJI fleets managed via FH2 web GCS

Configuration: FH2 workspace ID, device alias, MQTT broker endpoint, telemetry topic filter, OSD subscription set

Served to the DJI Runner on session establish

DJI Cloud API

For: Direct DJI Cloud API integration (server-to-server, no FH2)

Configuration: Cloud API endpoint, application credentials reference, device binding token, DRC command channel enablement

Used when a drone is enrolled with DJI Cloud rather than FH2

ADS-B Configuration

For: Per-drone ADS-B provisioning intent

Configuration: ADS-B In/Out, surrogate mode, ICAO hex mode, TIS-B rebroadcast participation

Drives both CIFIB upstream gating and Flight_Live ownership tagging

Vendor credentials, not in the configuration row. The configuration document references secrets by reference ({ "credential_ref": "vault://airmarket/dji-cloud/app-key" }), not by value. Actual secret material is held in a secrets store and resolved at runtime by the Custodian using its tenant-scoped service identity. tenant.drones never holds plaintext credentials.

3.6 Data Source Monitoring — The Vigilance Obligation

Every Custodian is responsible for knowing whether data is actually arriving from the aircraft, and at what rate and latency. A silent source is worse than a failed source, because a silent source looks the same as a healthy one until an operator notices the position hasn't moved.

StatusTriggerWhat downstream consumers do
HEALTHYRate within ±20% of publish_rate_hz and p95 latency below data_source_max_latency_ms.Position on map is authoritative. OI Automation uses it directly.
DEGRADEDRate below expected but still delivering, or p95 latency exceeded but data still arriving.Map shows a warning halo. OI Automation continues but flags OIs generated during degradation. OCC is notified.
STALENo message received in the last N × the expected interval (default N = 5).Map suppresses live position; renders last-known with a stale marker. OI Automation pauses regeneration. Alert raised.
DISCONNECTEDSource has explicitly disconnected.Same as STALE plus the map badge changes to off-network. Mission binding held; operator can re-attach source.
Rate and latency are per-source, not per-frame. The Custodian computes rate over rolling windows (default 5 seconds) so a single missed frame doesn't flip status. Latency is measured across many frames and reported as p50 / p95.

3.7 DAA Verification — Parallel AMQP Consumers, Not a Relay

Design change from the pre-v2.0 model Previously modeled: RTM Core publishes the avoidance command to the Custodian, which relays it to the Runner. Revised: the Custodian is no longer a delivery hop. RTM Core publishes once; the Custodian and the Runner each independently subscribe to the same message stream and receive every message in parallel. The Runner acts the moment it receives its copy — it does not wait on the Custodian. The Custodian, having received the identical message, independently verifies via the aircraft's real telemetry that the Runner actually did what the message said, and writes that confirmation (or a mismatch) to tenant.drones as tiered DAA status.

This mirrors TCAS-style monitoring rather than gating: the safety-critical path (RTM Core → Runner) has no extra hop that can add latency or become a silent single point of failure, and the Custodian becomes a true independent check rather than a relay that could itself fail to pass something along. See Fig. 2 above and Β§9 for the full DAA execution model this verification loop backs.

MessageChannelPriority
Avoidance commandAMQP only β€” flight declaration's own fanout exchangecritical
Proximity alert, active or resolvedAMQP and MQTT topic daa_alertsinfo / warning / critical by alert level
Implementation requirement β€” flag to Maykon & Raman A standard AMQP queue is competing-consumer: if the Custodian and Runner both bind to the same queue, messages split between them rather than both receiving every message. To get both a full copy of every message, RTM Core must publish to a fanout or topic exchange, with the Custodian and Runner each binding their own queue to it. This is real RabbitMQ/AMQP topology work, not just a diagram relabel.

4. State Lives in tenant.drones

The dTwin is a row in tenant.drones. The Custodian maintains it. Everything that wants to know the current state of an aircraft — including its DAA verification status — reads through the API surface that sits on top of this table. This section is fully self-contained: every schema the Custodian reads or writes, including the v2.0 DAA-verification columns, is reproduced here in full rather than pointed at v1.13.

DLB Field Cross-Reference

The table below maps every field the FLYsafe.software DLB exposes for a drone to its role in the Cloud Custodian. Stored in dlb_drones fields are persisted verbatim in drones.dlb_drones and surfaced through the v_dtwin view — they are never copied into drones.drones. Seeds RTM default fields supply initial values the Import process seeds into drones.drones on first insert; operators may later override via Custodian APIs. FK link fields are written to drones.drones once to establish the foreign key relationship. This table is unchanged from v1.13 — the DLB import contract is orthogonal to the Custodian/Runner DAA redesign.

DLB Field (exact name) Example Value Role Maps to / Note
guid 02150819-48AC-A3C6-… FK link PK in drones.dlb_drones; stored as dlb_guid in drones.drones as a FK on first insert. Links the two tables. All other DLB identity fields are read from dlb_drones via v_dtwin using this key.
serial_number 14334 FK link Also written to drones.drones on first insert — forms the composite PK (tenant_id, serial_number), the binding key across all systems.
name AIRTL01 Stored in dlb_drones Exposed as drone_name in v_dtwin. Not copied to drones.drones.
brand UAVSystemsinternatinoal Stored in dlb_drones Aircraft manufacturer. Available via v_dtwin.
model UAVSYSTEMSX6C Stored in dlb_drones Aircraft model identifier. Available via v_dtwin.
drone_type Hexacopter Stored in dlb_drones Frame classification (Hexacopter, Fixed-wing, etc.). Available via v_dtwin.
identification_number C-2109072432 Stored in dlb_drones TC / regulatory registration. Exposed as TC_Registration in v_dtwin. Not copied to drones.drones.
inventory_number (empty in example) Stored in dlb_drones Operator-assigned callsign used for ADS-B routing via AADMS. Exposed as Callsign in v_dtwin. Not copied to drones.drones.
status Airworthy Stored in dlb_drones Airworthiness status (Airworthy, Grounded, etc.). Available via v_dtwin.
max_horizontal_speed 15 Seeds RTM default Seeds horizontal_speed_ms (m/s) on first import. Operator may override via PUT /drones/{serial}/oi-defaults.
max_vertical_speed 10 Seeds RTM default Seeds vertical_speed_ms (m/s) on first import. Operator may override via PUT /drones/{serial}/oi-defaults.
hardware_version, firmware_version, propulsion_type, payload_capacity, weight, purchase_date, max_flight_time, insurable_value, color, company_guid, user_guid, controller_serial_number(2), flight_controller_serial_number, tech_number, notes various Stored / informational Persisted verbatim in drones.dlb_drones for reference; available via v_dtwin. None of these are copied into drones.drones.
Two-schema approach. The Import process writes all DLB fields — column names unchanged — into drones.dlb_drones (raw staging table). A view (drones.v_dlb_drones) renames the two display identifiers (inventory_number → Callsign, identification_number → TC_Registration) for operator-facing display and reporting. The main Custodian dTwin table (drones.drones) carries a curated subset of DLB fields under RTM-friendly names alongside RTM-managed configuration, live telemetry state, and — new in v2.0 — DAA verification state.

Schema: Raw DLB Import Table (drones.dlb_drones)

Stores every field from the DLB API with column names exactly as they arrive. This is the upsert target for the Import process (Steve Smith). No renaming — the schema matches the DLB source verbatim. Unchanged from v1.13.

-- drones.dlb_drones Β· /tenant/rtm-app-db/schema/dlb_drones.sql
-- Raw DLB import table. Column names match the DLB API verbatim.
-- Written by the Import process (Steve Smith). Never modified by RTM side.
CREATE TABLE IF NOT EXISTS drones.dlb_drones (

  -- DLB primary identity (exact field names from DLB API)
  guid                             TEXT        NOT NULL,        -- DLB drone UUID
  serial_number                    TEXT        NOT NULL,
  name                             TEXT,                        -- drone display name
  brand                            TEXT,
  model                            TEXT,
  drone_type                       TEXT,
  status                           TEXT,                        -- e.g. Airworthy, Grounded
  color                            TEXT,

  -- Regulatory / operational identifiers
  identification_number            TEXT,                        -- TC registration  (AS TC_Registration in v_dtwin)
  inventory_number                 TEXT,                        -- operator callsign (AS Callsign in v_dtwin)

  -- Hardware & firmware
  hardware_version                 TEXT,                        -- e.g. Pixhawk Cube
  firmware_version                 TEXT,                        -- e.g. 4.0.7
  flight_controller_serial_number  TEXT,
  controller_serial_number         TEXT,
  controller_serial_number2        TEXT,

  -- Performance specs (stored as TEXT to match DLB source; cast on use)
  max_horizontal_speed             TEXT,                        -- seeds horizontal_speed_ms in drones.drones
  max_vertical_speed               TEXT,                        -- seeds vertical_speed_ms in drones.drones
  max_flight_time                  TEXT,                        -- rated endurance (minutes)
  payload_capacity                 TEXT,                        -- kg
  weight                           TEXT,
  propulsion_type                  TEXT,                        -- ELECTRIC | ICE | HYBRID

  -- Ownership & financial
  company_guid                     TEXT,
  user_guid                        TEXT,
  insurable_value                  TEXT,
  purchase_date                    TEXT,                        -- ISO-8601 string from DLB; cast on use
  tech_number                      TEXT,
  notes                            TEXT,

  -- Import bookkeeping
  imported_at                      TIMESTAMPTZ NOT NULL DEFAULT now(),

  PRIMARY KEY (guid)
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_dlb_drones_serial
  ON drones.dlb_drones (serial_number);

Schema: Custodian dTwin Table (drones.drones)

The canonical per-drone RTM state table maintained by the Cloud Custodian. Holds only RTM-specific data — configuration, live state, DAA verification state, and the link key (dlb_guid) back to drones.dlb_drones. All DLB identity fields live exclusively in dlb_drones and are accessed via the v_dtwin view. Everything through data_source_* is unchanged from v1.13; the DAA verification block is new in v2.0, backing the Custodian's fourth obligation (Β§2) and the parallel-AMQP-consumer model (Β§3.7).

-- /tenant/rtm-app-db/schema/drones.sql
-- One row per drone per tenant. Maintained by the Custodian. PK = (tenant, serial).
-- DLB identity data lives in drones.dlb_drones; join via dlb_guid. Use v_dtwin for the full dTwin record.
CREATE SCHEMA IF NOT EXISTS drones;

CREATE TABLE drones.drones (
  -- Core identity
  tenant_id                         text NOT NULL,
  serial_number                     text NOT NULL,
  dlb_guid                          text NOT NULL,            -- FK → drones.dlb_drones.guid

  -- Custodian configuration (RTM-managed; written via Custodian APIs)
  adapter_type                      text DEFAULT 'mavlink'::text NOT NULL,   -- mavlink | dji_fh2 | dronesense
  adapter_config                    jsonb DEFAULT '{}'::jsonb NOT NULL,
  publish_rate_hz                   int4 DEFAULT 4 NOT NULL,

  -- OI defaults & FLYplan automation (speeds seeded from dlb_drones on first insert)
  oi_automation_enabled             bool DEFAULT false NOT NULL,     -- pilot-granted authority; see Β§7
  horizontal_speed_ms               float8 DEFAULT 15.0 NOT NULL,   -- seeded from dlb_drones.max_horizontal_speed
  vertical_speed_ms                 float8 DEFAULT 6.0 NOT NULL,    -- seeded from dlb_drones.max_vertical_speed

  -- Flight Volume (FV) / Contingency Volume (CV) buffers — horizontal only; vertical separation
  -- from other traffic is handled by the NMAC/SWC crewed/uncrewed minima below, not by FV/CV.
  fv_buffer_m                       float8 DEFAULT 56.0 NOT NULL,    -- horizontal buffer around flight path
  cv_buffer_m                       float8 DEFAULT 150.0 NOT NULL,   -- additional horizontal buffer past FV edge

  -- OI geometry parameters (see Β§5A)
  oi_automation_square_m            float8 DEFAULT 700.0 NOT NULL,   -- OI square side (m); valid 350–1400
  oi_automation_radius_m            float8 DEFAULT 400.0 NOT NULL,   -- OI radius (m); valid 200–800
  oi_automation_amend_min_trigger_m float8 DEFAULT 100.0 NOT NULL,   -- OI amend min trigger (m); valid 50–200
  oi_automation_amend_max_alt_agl_m float8 DEFAULT 300.0 NOT NULL,   -- OI amend max altitude AGL (m)
  oi_automation_amend_enabled       bool DEFAULT false NOT NULL,

  -- Flight monitoring — radius within which a mission waypoint is considered reached.
  waypoint_buffer_m                 float8 DEFAULT 50.0 NOT NULL,    -- valid 5–500

  -- Default pilot binding — pre-filled pilot when a mission is created without explicit selection
  default_pilot_name                text NULL,
  default_pilot_fs_guid             text NULL,               -- FLYsafe.software pilot GUID

  -- Separation defaults — per-drone conflict-evaluation minima, metres, split crewed/uncrewed × horizontal/vertical.
  nmac_h_crewed_m                   float8 DEFAULT 150.0  NOT NULL,  -- NMAC horizontal, crewed traffic
  nmac_v_crewed_m                   float8 DEFAULT 30.0   NOT NULL,  -- NMAC vertical, crewed traffic
  nmac_h_uncrewed_m                 float8 DEFAULT 15.0   NOT NULL,  -- NMAC horizontal, uncrewed traffic
  nmac_v_uncrewed_m                 float8 DEFAULT 8.0    NOT NULL,  -- NMAC vertical, uncrewed traffic
  swc_h_crewed_m                    float8 DEFAULT 1500.0 NOT NULL,  -- SWC horizontal, crewed traffic
  swc_v_crewed_m                    float8 DEFAULT 150.0  NOT NULL,  -- SWC vertical, crewed traffic
  swc_h_uncrewed_m                  float8 DEFAULT 150.0  NOT NULL,  -- SWC horizontal, uncrewed traffic
  swc_v_uncrewed_m                  float8 DEFAULT 30.0   NOT NULL,  -- SWC vertical, uncrewed traffic
  eval_distance_m                   float8 DEFAULT 1852.0 NOT NULL,  -- Evaluation ring (~1 NM)

  -- ADS-B provisioning
  adsb_in_enabled                   bool DEFAULT false NOT NULL,
  adsb_out_enabled                  bool DEFAULT false NOT NULL,
  adsb_surrogate_enabled            bool DEFAULT false NOT NULL,
  tisb_rebroadcast_enabled          bool DEFAULT false NOT NULL,     -- enables rebroadcast of this aircraft’s own telemetry to TIS-B

  -- Conspicuity / ADS-B detail — provisioning detail behind the flags above.
  ec_adsb_hex                       text NULL,                       -- static ADS-B ICAO hex; set when adsb_hex_mode = 'static'
  adsb_out_device                   text NULL,                       -- transponder model, e.g. Ping978ec | Ping2020i | SkyEcho 2 | TISB | other
  adsb_hex_mode                     text DEFAULT 'dynamic'::text NOT NULL,   -- dynamic (ADDRt) | static (user-entered, see ec_adsb_hex)
  surveillance_mode                 text NULL,                       -- descriptive surveillance method, e.g. "MLAT + ADS-B", "ADS-B + Vision"
  daa_enabled                       bool DEFAULT false NOT NULL,     -- Detect-and-Avoid active for this drone (provisioning intent β€” see daa_status below for outcome)
  broadcast_binding                 text NULL,                       -- e.g. Bound | Unbound — binding status between this drone and its broadcast identity
  networkid_streaming_enabled       bool DEFAULT false NOT NULL,     -- gates NetworkID into RTM viz + DAA; independent of tisb_rebroadcast_enabled

  -- Observed ADS-B state (read-only) — from surveillance feeds, not the provisioning intent above
  adsb_observed_broadcasting            bool NULL,
  adsb_observed_last_icao_hex           text NULL,
  adsb_observed_last_seen_by_cifib_at   timestamptz NULL,             -- last seen by the CIFIB upstream gating pipeline

  -- DAA verification (v2.0) — the Custodian's fourth obligation; see Β§2, Β§3.7, Β§9
  daa_status                        text NULL,               -- FULL | DEGRADED | NO_QUALIFIED_SURVEILLANCE
  current_avoidance_maneuver_id     text NULL,               -- FK to the AMQP avoidance_maneuver message's alert_id, while active
  current_avoidance_state           text NULL,               -- NONE | COMMANDED | EXECUTING | CONFIRMED | MISMATCH | CLEARED
  last_daa_verification_at          timestamptz NULL,         -- last time the Custodian compared Runner telemetry against a published command

  -- Ownership flag (see Β§8)
  ownership_flag_enabled            bool DEFAULT false NOT NULL,

  -- Data source monitoring — the Custodian’s vigilance obligation; see Β§3.6
  data_source_mechanism             text NULL,               -- mavlink_runner | dji_runner | dji_cloud | adsb | multi
  data_source_rate_hz               float8 NULL,
  data_source_latency_ms_p50        float8 NULL,
  data_source_latency_ms_p95        float8 NULL,
  data_source_status                text NULL,               -- HEALTHY | DEGRADED | STALE | DISCONNECTED
  data_source_max_latency_ms        int4 DEFAULT 2000 NOT NULL,      -- expected envelope
  data_source_last_seen_at          timestamptz NULL,

  -- Current flight — the mission this drone is executing right now.
  -- Populated by FLYsafe.software on mission publish; progress fields updated by the Custodian from telemetry.
  current_mission_id                text NULL,
  current_flight_state              text NULL,               -- PLANNED | ACTIVE | COMPLETED | ABORTED
  current_flight_window_start       timestamptz NULL,
  current_flight_window_end         timestamptz NULL,
  current_waypoint_index            int4 NULL,
  current_waypoint_total            int4 NULL,
  current_progress_pct              int4 NULL,

  -- Current OI — the currently active Operational Intent for this drone.
  -- Written by the Flink OI Automation job on every generation/regeneration.
  current_oi_id                     text NULL,
  current_oi_state                  text NULL,               -- DRAFT | SUBMITTED | ACCEPTED | ACTIVE | COMPLETED | REJECTED
  current_oi_source                 text NULL,               -- AUTOMATED | MANUAL | MISSION_PUBLISH
  current_oi_window_start           timestamptz NULL,
  current_oi_window_end             timestamptz NULL,
  oi_last_regenerated_at            timestamptz NULL,
  oi_next_regeneration_at           timestamptz NULL,

  -- Live state (Custodian writes these from Autopilot Runner telemetry)
  position_lat                      float8 NULL,
  position_lon                      float8 NULL,
  position_alt                      float8 NULL,
  flight_mode                       text NULL,
  armed                              bool NULL,
  battery_remaining_pct             int4 NULL,
  link_rssi_dbm                     int4 NULL,
  adapter_connected                 bool DEFAULT false NOT NULL,
  last_telemetry_at                 timestamptz NULL,

  -- Cross-references
  aadms_id                          text NULL,               -- cross-tenant identity reference

  -- Timestamps
  created_at                        timestamptz DEFAULT now() NOT NULL,
  updated_at                        timestamptz DEFAULT now() NOT NULL,
  retired_at                        timestamptz NULL,

  CONSTRAINT drones_pkey PRIMARY KEY (tenant_id, serial_number),
  CONSTRAINT drones_dlb_guid_fk FOREIGN KEY (dlb_guid) REFERENCES drones.dlb_drones(guid),
  CONSTRAINT drones_dlb_guid_unique UNIQUE (dlb_guid)
);

CREATE INDEX idx_drones_serial   ON drones.drones USING btree (serial_number);
CREATE INDEX idx_drones_daa_status ON drones.drones USING btree (daa_status)   -- v2.0
  WHERE daa_status IS NOT NULL;
Relationship between daa_enabled and the new DAA verification columns. daa_enabled is the per-drone provisioning intent — whether DAA is turned on for this aircraft at all — and drones.daa_rules (below) is the ordered rule set the Conspicuity/DAA panel edits. daa_status, current_avoidance_maneuver_id, current_avoidance_state, and last_daa_verification_at are downstream of both: they record the outcome of DAA being active — what RTM Core commanded, what state the Runner reached, and whether the Custodian's independent check confirmed it.

Schema: dTwin View (drones.v_dtwin)

Read-only view that joins drones.drones with drones.dlb_drones on the dlb_guid foreign key. This is the authoritative dTwin data record — the single consistent view consumers should use when they need both RTM state and DLB identity together. The Custodian APIs serve responses that reflect this aggregated record, now including the DAA verification block.

-- drones.v_dtwin Β· /tenant/rtm-app-db/schema/v_dtwin.sql
-- dTwin view: joins drones.drones (RTM state) with drones.dlb_drones (DLB identity).
CREATE OR REPLACE VIEW drones.v_dtwin AS
SELECT
  d.tenant_id, d.serial_number, d.dlb_guid,

  -- DLB identity (from dlb_drones; renamed for clarity)
  dlb.name AS drone_name, dlb.brand, dlb.model, dlb.drone_type, dlb.status, dlb.color,
  dlb.identification_number AS "TC_Registration",
  dlb.inventory_number      AS "Callsign",
  dlb.hardware_version, dlb.firmware_version, dlb.max_horizontal_speed, dlb.max_vertical_speed,
  dlb.max_flight_time, dlb.payload_capacity, dlb.weight, dlb.propulsion_type,

  -- Custodian configuration, OI defaults, pilot & separation defaults, ADS-B provisioning
  d.adapter_type, d.adapter_config, d.publish_rate_hz,
  d.oi_automation_enabled, d.horizontal_speed_ms, d.vertical_speed_ms,
  d.fv_buffer_m, d.cv_buffer_m, d.oi_automation_square_m, d.oi_automation_radius_m,
  d.oi_automation_amend_min_trigger_m, d.oi_automation_amend_max_alt_agl_m, d.oi_automation_amend_enabled,
  d.waypoint_buffer_m, d.default_pilot_name, d.default_pilot_fs_guid,
  d.nmac_h_crewed_m, d.nmac_v_crewed_m, d.nmac_h_uncrewed_m, d.nmac_v_uncrewed_m,
  d.swc_h_crewed_m, d.swc_v_crewed_m, d.swc_h_uncrewed_m, d.swc_v_uncrewed_m, d.eval_distance_m,
  d.adsb_in_enabled, d.adsb_out_enabled, d.adsb_surrogate_enabled, d.tisb_rebroadcast_enabled,
  d.ec_adsb_hex, d.adsb_out_device, d.adsb_hex_mode, d.surveillance_mode, d.daa_enabled,
  d.broadcast_binding, d.networkid_streaming_enabled,
  d.adsb_observed_broadcasting, d.adsb_observed_last_icao_hex, d.adsb_observed_last_seen_by_cifib_at,

  -- DAA verification (v2.0) β€” see Β§2, Β§3.7, Β§9
  d.daa_status,
  d.current_avoidance_maneuver_id,
  d.current_avoidance_state,
  d.last_daa_verification_at,

  d.ownership_flag_enabled,

  -- Data source monitoring β€” see Β§3.6
  d.data_source_mechanism, d.data_source_rate_hz, d.data_source_latency_ms_p50, d.data_source_latency_ms_p95,
  d.data_source_status, d.data_source_max_latency_ms, d.data_source_last_seen_at,

  -- Current flight & current OI
  d.current_mission_id, d.current_flight_state, d.current_flight_window_start, d.current_flight_window_end,
  d.current_waypoint_index, d.current_waypoint_total, d.current_progress_pct,
  d.current_oi_id, d.current_oi_state, d.current_oi_source, d.current_oi_window_start, d.current_oi_window_end,
  d.oi_last_regenerated_at, d.oi_next_regeneration_at,

  -- Live state
  d.position_lat, d.position_lon, d.position_alt, d.flight_mode, d.armed, d.battery_remaining_pct,
  d.link_rssi_dbm, d.adapter_connected, d.last_telemetry_at,

  -- Cross-references & timestamps
  d.aadms_id, d.created_at, d.updated_at, d.retired_at

FROM drones.drones d
JOIN drones.dlb_drones dlb ON dlb.guid = d.dlb_guid;

Schema: AADMS Materialized View (aadms.drones)

Cross-tenant drone lookup table used by the ADS-B routing pipeline. A materialized view that aggregates drones.drones (joined to dlb_drones for callsign) from every tenant schema into a single flat surface. ADS-B records arrive carrying only callsign; AADMS resolves callsign → (tenant_id, serial_number). Unchanged from v1.13.

-- aadms.drones Β· cross-tenant drone lookup
CREATE SCHEMA IF NOT EXISTS aadms;

CREATE MATERIALIZED VIEW aadms.drones AS
  SELECT d.tenant_id, d.serial_number, d.dlb_guid AS guid,
         dlb.inventory_number AS callsign, d.aadms_id AS id
  FROM tenant_airmarket.drones.drones d
  JOIN tenant_airmarket.drones.dlb_drones dlb ON dlb.guid = d.dlb_guid
  WHERE d.retired_at IS NULL
  UNION ALL
  SELECT d.tenant_id, d.serial_number, d.dlb_guid AS guid,
         dlb.inventory_number AS callsign, d.aadms_id AS id
  FROM tenant_sait.drones.drones d
  JOIN tenant_sait.drones.dlb_drones dlb ON dlb.guid = d.dlb_guid
  WHERE d.retired_at IS NULL
  -- ... repeat for each tenant ...
;

CREATE UNIQUE INDEX idx_aadms_drones_callsign ON aadms.drones (callsign);
CREATE INDEX        idx_aadms_drones_serial   ON aadms.drones (serial_number);
Refresh strategy. Refreshed with REFRESH MATERIALIZED VIEW CONCURRENTLY aadms.drones at the end of each Import sync run. DEV and PROD use separate AADMS schemas so refreshes are independent.
OI geometry storage. fv_buffer_m/cv_buffer_m are configuration — a single horizontal buffer distance. The three computed geometries (Flight Volume, Contingency Volume, Submitted Volume) for current and upcoming flights are persisted in the flights_binding table, outside the drones schema — not in a drones.oi_geometry table (that was specified in early drafts but never built). See Β§12 for the API that computes these three geometries.

Schema: DAA Rules Store (drones.daa_rules)

One row per Detect-and-Avoid rule per drone — the ordered rule set the Conspicuity/DAA panel edits as daa_rules[]. Rules are evaluated in sort_order; each can be individually enabled/disabled without deleting it. Unchanged from v1.13.

-- drones.daa_rules Β· one row per DAA rule per drone
-- Read/written by the Custodian API's DAA endpoints; evaluated by the DAA service at runtime.
CREATE TABLE drones.daa_rules (
  tenant_id             text NOT NULL,
  serial_number         text NOT NULL,
  rule_id                text NOT NULL,                             -- e.g. R1, R2, R3

  condition_text        text NOT NULL,                              -- human-readable trigger condition, e.g. "Target within NMAC radius"
  action_text            text NOT NULL,                              -- action taken when triggered, e.g. "RTH β€” Immediate"
  priority                text NOT NULL,                              -- critical | high | medium | low
  enabled                 bool NOT NULL DEFAULT true,
  sort_order             int4 NOT NULL DEFAULT 0,                    -- evaluation / display order

  PRIMARY KEY (tenant_id, serial_number, rule_id),
  FOREIGN KEY (tenant_id, serial_number) REFERENCES drones.drones(tenant_id, serial_number)
);

How drones.dlb_drones and drones.drones work together

drones.dlb_drones is the primary store for all raw DLB API data, written by the Import process and never touched by RTM. drones.drones holds only RTM state — configuration, live telemetry, DAA verification, and the dlb_guid foreign key. Three distinct processes write to these tables at different times; each owns specific columns and must not write to columns owned by another.

Three processes, three write surfaces

1 DLB API → dlb_drones The Import process (Steve Smith) fetches all drone records from the DLB API and upserts every field into drones.dlb_drones. On first insert, also creates the corresponding row in drones.drones with dlb_guid and seeds the two speed defaults.
2 Custodian APIs → drones.drones Operators (via the RTM Map UI) and MCP agents write RTM-managed columns through the Custodian APIs — adapter config, OI defaults and automation parameters, ADS-B provisioning flags, ownership flag.
3 Autopilot Runner → drones.drones v2.0 scope The Custodian updates the live-state columns on every telemetry frame the Runner forwards (position_*, flight_mode, armed, battery_remaining_pct, adapter_connected, last_telemetry_at) and the DAA verification columns whenever it cross-checks a POST /avoidance/report from the Runner against that same telemetry (daa_status, current_avoidance_state, last_daa_verification_at). Both are Custodian-authored writes triggered by Runner input — neither the Import process nor operators write these columns directly.
v_dtwin (read-only) Joins all three surfaces into a single consistent dTwin record, DAA verification block included. The Custodian API responses, frontend UI, and MCP tools all read from this view.
CategoryPrimary storeWriterExamples
DLB identity drones.dlb_drones Import script (Steve Smith) name, brand, model, identification_number, inventory_number
RTM-managed drones.drones Custodian APIs (operator UI, MCP agents) adapter_type, oi_automation_enabled, NMAC/SWC/EVAL minima, ADS-B flags, ownership_flag_enabled
Live state drones.drones Custodian, from Runner telemetry position_lat/lon/alt, flight_mode, armed, adapter_connected, last_telemetry_at
DAA verification New in v2.0 drones.drones Custodian, cross-referencing Runner's avoidance/report against telemetry daa_status, current_avoidance_maneuver_id, current_avoidance_state, last_daa_verification_at
One-way DLB sync; cadence TBD. The Import process runs hourly or nightly — final cadence is an open question. drones.dlb_drones is overwritten on every sync run. drones.drones is only touched by the Import process on first insert. RTM-managed, live-state, and DAA-verification columns are never in scope for the Import process.

The dTwin: what it actually is

The dTwin is the conceptual digital twin of one aircraft — the canonical answer to “what does FLYsafe.live believe is true about this drone right now?” It is the v_dtwin record for that drone. This is the complete GET /drones/{serial} response — every block, including the new daa block.

{
  "identity": {
    "serial_number":   "1581F5BBB1F2A",
    "tc_registration": "C-FAIRTL69",
    "callsign":        "AIRTL69",
    "tenant":          "airmarket",
    "adapter_type":    "mavlink"
  },
  "position":    { "lat": 53.5461, "lon": -113.4938, "alt_msl_m": 720.4 },
  "flight_mode": "AUTO",
  "armed":       true,
  "battery":     { "remaining_pct": 68 },
  "adsb": {
    "in_enabled": true, "out_enabled": true,
    "surrogate_enabled": false, "ownership_flagged": true
  },
  "pilot": { "default_name": "Jane Chen", "default_fs_guid": "pilot-8f3a2e19-4c1a-4e6b-9c17-2b9e5d3f7a0c" },
  "oi": {
    "automation_enabled": true, "default_distance_m": 300, "waypoint_buffer_m": 50.0
  },
  "volumes": { "flight_volume": { "buffer_m": 56.0 }, "contingency_volume": { "buffer_m": 150.0 } },
  "separation": {
    "nmac_crewed_m":   { "h": 150.0,  "v": 30.0 },
    "nmac_uncrewed_m": { "h": 15.0,   "v": 8.0 },
    "swc_crewed_m":    { "h": 1500.0, "v": 150.0 },
    "swc_uncrewed_m":  { "h": 150.0,  "v": 30.0 },
    "eval_distance_m": 1852.0
  },
  "data_source": {
    "mechanism": "mavlink_runner", "rate_hz": 4.0,
    "latency_ms_p50": 180, "latency_ms_p95": 420, "max_latency_ms": 2000,
    "status": "HEALTHY", "last_seen_at": "2026-09-05T14:22:18.315Z"
  },
  "current_flight": {
    "mission_id": "mission-airmarket-2026090514200", "state": "ACTIVE",
    "window": { "start": "2026-09-05T14:00:00Z", "end": "2026-09-05T14:45:00Z" },
    "waypoints": { "current_index": 5, "total": 12 }, "progress_pct": 42
  },
  "current_oi": {
    "oi_id": "oi-airmarket-2026090514201", "state": "ACTIVE", "source": "AUTOMATED",
    "last_regenerated_at": "2026-09-05T14:21:15Z", "next_regeneration_at": "2026-09-05T14:21:45Z",
    "geometry_ref": {
      "flight_volume":      "/api/v1/drones/1581F5BBB1F2A/oi/current/flight-volume",
      "contingency_volume": "/api/v1/drones/1581F5BBB1F2A/oi/current/contingency-volume",
      "submitted_volume":   "/api/v1/drones/1581F5BBB1F2A/oi/current"
    }
  },
  "daa": {                                       // v2.0 β€” the Custodian's verification obligation, Β§2/Β§3.7
    "enabled":        true,                    // drones.drones.daa_enabled
    "status":         "FULL",                    // FULL | DEGRADED | NO_QUALIFIED_SURVEILLANCE
    "active_maneuver": {
      "maneuver_id":   "alert-7f2a9c31-4e5b-4c8a-9d21-8b6f3e2a1c04",
      "state":         "CONFIRMED",          // NONE | COMMANDED | EXECUTING | CONFIRMED | MISMATCH | CLEARED
      "commanded_at":  "2026-09-05T14:22:03.100Z",
      "verified_at":   "2026-09-05T14:22:04.850Z"
    },
    "last_verification_at": "2026-09-05T14:22:04.850Z"
  },
  "adapter_health": { "connected": true, "last_publish_ago_ms": 247 },
  "timestamps": { "updated_at": "2026-09-05T14:22:18.523Z", "source_ts": "2026-09-05T14:22:18.491Z" }
}

↑ Returned by GET /drones/{serial}. There is no canonical “dTwin payload schema” separate from the API response — the response is the schema. Cheaper subset endpoints (/state, /current-flight, /current-oi, /data-source, /daa-status) exist for consumers that don’t want the full payload every time.

What happened to MQTT? MQTT topic distribution of the dTwin is not the primary contract. Consumers that need a push feed of changes subscribe to the API's WebSocket watch endpoints (including /daa-status/watch, v2.0). The system of record is the Postgres row.

5. Cloud Custodian APIs

This is the complete catalog of API operations the Cloud Custodian exposes, categories A through K, fully specified. The same APIs are consumed by:

All endpoints are tenant-scoped (the tenant is resolved from the JWT). The base path is /api/v1. Verbs use REST conventions: GET reads, POST creates/triggers, PUT updates, DELETE removes. Where a notification stream is useful, a WebSocket endpoint is offered as a parallel option to polling.

Implementation pattern. The API is implemented as a thin layer over Postgres views (Raman’s pattern). Each consumer (operator UI, Runner, MCP) reads through a view that projects the appropriate columns. Writes go through view triggers that update drones.drones.
Cat.CategoryCovers
AFLYplan & OI AutomationAutomated OI creation, per-drone OI defaults, OI automation toggling, FV/CV buffers, waypoint-arrival buffer.
BADS-B ProvisioningIn/Out/surrogate flags, TIS-B rebroadcast, observed ADS-B status.
COwnership FlagOperator's surveillance-intent flag for own-aircraft tagging.
DCustodian ConfigurationAdapter type, Runner endpoints, publish rate, OI defaults bundled as the Runner's boot config.
EState Read & Telemetry IngestionFull/partial state reads; the ingestion endpoint the Autopilot Runner posts to.
FMission BindingBinds an active mission to a drone for an OI window.
GIdentity & Cross-Tenant RoutingKeycloak registration, AADMS publication.
HHealth & LifecycleCustodian health probe; retire/restore.
IPilot & Separation DefaultsDefault pilot binding; NMAC/SWC/EVAL minima.
JData Source MonitoringRead-only vigilance state surface.
KDAA Verification & Avoidance Status New in v2.0Tiered DAA status, active-maneuver state, and post-flight avoidance-event history from the Custodian's verification loop (Β§3.7).

A. FLYplan & OI Automation

Operations supporting the FLYplan workflow — automated OI creation, per-drone OI defaults, OI automation toggling, FV/CV volume buffers, and the waypoint-arrival buffer.

Pilot-granted OI Automation authority. The pilot grants authority explicitly by flipping the OI Automation toggle to on on the Inventory page of the RTM Map app. Once granted, the Custodian owns the state transitions autonomously. The pilot can revoke authority at any time.
EndpointPurposePrimary Caller
GET/drones/{serial}/oi-automationRead the OI Automation status for a drone (enabled, defaults, last regeneration time).Frontend Β· MCP
PUT/drones/{serial}/oi-automationEnable or disable OI Automation for this drone. Body: { enabled: bool }.Frontend Β· MCP
PUT/drones/{serial}/oi-defaultsSet OI default parameters: horizontal_speed_ms, vertical_speed_ms, oi_automation_square_m, oi_automation_radius_m, oi_automation_amend_min_trigger_m.Frontend
GET/drones/{serial}/oi-defaultsRead the current OI defaults.Frontend Β· MCP Β· Runner (boot)
GET/drones/{serial}/volumesRead the drone’s FV and CV buffer defaults: { flight_volume: { buffer_m }, contingency_volume: { buffer_m } }.Frontend Β· MCP Β· Flink Β· Runner (boot)
PUT/drones/{serial}/volumesSet FV and/or CV buffer defaults. Server-side clamp enforces fv_buffer_m ≤ cv_buffer_m.Frontend
GET/drones/{serial}/waypoint-bufferRead the waypoint-arrival buffer in metres (default 50 m).Frontend Β· MCP Β· Flink
PUT/drones/{serial}/waypoint-bufferSet the waypoint-arrival buffer. Server-side clamp: 5–500 m.Frontend
POST/drones/{serial}/oi/regenerateForce-regenerate the OI now. Backend/ops-tooling only.MCP
GET/drones/{serial}/oi/currentFetch the Submitted Volume — the GeoJSON polygon combining FV and CV, sent to FLYrtm DSS.Frontend Β· Flink
GET/drones/{serial}/oi/current/flight-volumeFetch the Flight Volume geometry alone as its own GeoJSON polygon.Frontend (Map) Β· Compliance AI
GET/drones/{serial}/oi/current/contingency-volumeFetch the Contingency Volume geometry alone. Always contains the Flight Volume.Frontend (Map) Β· Compliance AI
GET/drones/{serial}/current-oiFetch the current OI summary: { oi_id, state, source, window, last_regenerated_at, next_regeneration_at, geometry_ref }.Frontend Β· MCP Β· Compliance AI
WS/drones/{serial}/current-oi/watchSubscribe to current-OI state transitions and regeneration events.Frontend Β· OCC
GET/drones/{serial}/oi/historyList previous OI generations, including all three stored geometries per generation.Compliance AI Β· Evidence
Proportionality constraint. oi_automation_square_m, oi_automation_radius_m, and oi_automation_amend_min_trigger_m were tuned together and must scale together — valid range for each is 0.5× to 2.0× its default. The frontend presents them as a single factor slider rather than three independent inputs.

B. ADS-B Provisioning

Per-drone ADS-B configuration.

EndpointPurposePrimary Caller
GET/drones/{serial}/adsbRead ADS-B configuration block: in_enabled, out_enabled, surrogate_enabled.Frontend Β· MCP Β· Custodian
PUT/drones/{serial}/adsbUpdate the full ADS-B configuration block in one call.Frontend
PUT/drones/{serial}/adsb-surrogateToggle just the ADS-B Surrogate flag.Frontend Β· MCP
PUT/drones/{serial}/tisb-rebroadcastToggle the TIS-B Rebroadcast flag. Body: { enabled: bool }.Frontend Β· MCP
GET/drones/{serial}/adsb/statusRead the live ADS-B status from telemetry: is the transponder actually broadcasting?Frontend Β· Compliance AI

C. Ownership Flag

The operator’s explicit surveillance intent for one of their drones. When set, Flight_Live (Β§8) tags inbound records of this aircraft as own-aircraft.

EndpointPurposePrimary Caller
GET/drones/{serial}/ownershipRead the current ownership flag state.Frontend Β· Flight_Live
PUT/drones/{serial}/ownershipSet the ownership flag. Body: { enabled: bool, reason?: string }.Frontend Β· MCP (permission-scoped)

D. Custodian Configuration (adapter, defaults, boot)

The configuration block the Autopilot Runner fetches at boot — adapter type, Runner endpoints, broker addresses, publish rate, and OI defaults bundled as one document.

EndpointPurposePrimary Caller
GET/drones/{serial}/configRunner boot config — adapter type, publish rate, ADS-B block, OI defaults, ownership flag, etc.Runner (on boot)
GET/drones/{serial}/config?since={ts}Returns config only if changed since the given timestamp.Runner (watch loop)
WS/drones/{serial}/config/watchWebSocket subscription that pushes config changes as they happen.Runner (optional)
PUT/drones/{serial}/adapter-configUpdate adapter-specific connection block (MAVLink host/port, DJI FH2 endpoint, DRC channel enablement).Frontend
PUT/drones/{serial}/publish-rateChange the telemetry publish rate (Hz). Runner hot-reloads.Frontend Β· MCP

E. State Read & Telemetry Ingestion

State-read endpoints for consumers, and the ingestion endpoint the Autopilot Runner POSTs to.

EndpointPurposePrimary Caller
GET/dronesList drones for the tenant. Supports filters (active flight, callsign substring, model). Paginated.Frontend Β· MCP
GET/drones/{serial}Full drone record — identity, config, current live state, DAA status. The canonical dTwin view.All consumers
GET/drones/{serial}/stateJust the live-state subset without configuration noise.Map (live layer)
GET/drones/{serial}/current-flightFetch the current-flight block: { mission_id, state, window, waypoints, progress_pct }.Frontend Β· MCP Β· Compliance AI
WS/drones/{serial}/current-flight/watchSubscribe to flight-state transitions and waypoint progress.Frontend
GET/drones/{serial}/telemetryRecent telemetry timeseries within a time range. Backed by InfluxDB.Map (trails) Β· Evidence
WS/drones/{serial}/state/watchSubscribe to live state updates for one drone.Frontend (map live layer)
POST/drones/{serial}/telemetryIngestion endpoint. The Autopilot Runner POSTs normalized telemetry frames here. The Custodian writes the live-state columns and appends to InfluxDB.Autopilot Runner (MAVLink, DJI)
POST/drones/{serial}/alertsRunner-emitted alerts (mode change, RTL triggered, geofence breach, low battery).Autopilot Runner Β· OCC AI

F. Mission Binding

Binds an active mission to a drone for the duration of an OI window.

EndpointPurposePrimary Caller
GET/drones/{serial}/missionRead the active mission binding.Frontend Β· Flink Β· Compliance AI
POST/drones/{serial}/missionCreate a mission binding: mission_id, callsign, OI window, pilot_id.FLYsafe.software (on mission publish)
DELETE/drones/{serial}/missionClear the active mission binding.FLYsafe.software Β· Frontend

G. Identity & Cross-Tenant Routing

Identity registration in Keycloak and cross-tenant publication into AADMS.

EndpointPurposePrimary Caller
POST/drones/{serial}/identity/keycloakRegister the drone as a user in Keycloak. ID format <tenant>-<serial>. Idempotent.Import process Β· Custodian (boot)
POST/drones/{serial}/identity/aadmsPublish the drone’s (tenant, serial, callsign) tuple to AADMS.Import process Β· Custodian
GET/drones/{serial}/identityInspect identity state.Frontend (admin view)

H. Health & Lifecycle

Health probes for the Custodian, and lifecycle events.

EndpointPurposePrimary Caller
GET/drones/{serial}/healthHealth of the Custodian for this drone: adapter_connected, last_telemetry_at, runner_session_id, runner_version.OCC Β· Monitoring Β· Compliance AI
POST/drones/{serial}/retireMark drone as retired. Stops accepting telemetry, clears live state, archives the row.Frontend (admin)
POST/drones/{serial}/restoreRestore a retired drone.Frontend (admin)

I. Pilot & Separation Defaults

Per-drone default pilot binding and conflict-evaluation minima.

EndpointPurposePrimary Caller
GET/drones/{serial}/default-pilotRead the default pilot: { name, fs_guid }.Frontend Β· MCP
PUT/drones/{serial}/default-pilotSet the default pilot. fs_guid validated against FLYsafe.software’s pilot registry.Frontend
DELETE/drones/{serial}/default-pilotClear the default pilot binding.Frontend
GET/drones/{serial}/separation-defaultsRead per-drone NMAC, SWC, EVAL distances, split crewed/uncrewed × horizontal/vertical.Frontend Β· MCP Β· Flink
PUT/drones/{serial}/separation-defaultsSet NMAC, SWC, and EVAL distances. Server-side validation enforces nmac < swc < eval.Frontend

J. Data Source Monitoring

Read-only surface for the vigilance state the Custodian maintains for each drone (Β§3.6).

EndpointPurposePrimary Caller
GET/drones/{serial}/data-sourceRead the summary source-health block: { mechanism, rate_hz, latency_ms_p50, latency_ms_p95, max_latency_ms, status, last_seen_at }.Frontend (Map) Β· OCC Β· MCP
PUT/drones/{serial}/data-source/envelopeSet the expected envelope: { max_latency_ms }.Frontend (admin)
WS/drones/{serial}/data-source/watchWebSocket that pushes status transitions as they happen.OCC AI (live)
GET/drones/{serial}/data-source/historyTime-range query of status transitions and rate/latency samples. Feeds CAR 922 evidence.Compliance AI Β· Evidence

K. DAA Verification & Avoidance Status New in v2.0

Read-only surface for the Custodian's Verification obligation (Β§2). Backs the tiered DAA status RTM Core consumes and gives operators/OCC/Compliance AI visibility into every avoidance event without polling raw telemetry.

EndpointPurposePrimary Caller
GET/drones/{serial}/daa-statusRead the current tiered DAA status block: { enabled, status, active_maneuver, last_verification_at }. status is FULL, DEGRADED, or NO_QUALIFIED_SURVEILLANCE.Frontend (Map) Β· OCC Β· RTM Core Β· MCP
WS/drones/{serial}/daa-status/watchSubscribe to current_avoidance_state transitions (COMMANDEDEXECUTINGCONFIRMED or MISMATCHCLEARED) as the Custodian's verification loop observes them.Frontend Β· OCC AI
GET/drones/{serial}/avoidance/historyTime-range query of past avoidance events: command received, Runner state reached, verification outcome, elapsed time to confirmation. Feeds CAR 922 evidence.Compliance AI Β· Evidence Β· OCC
POST/drones/{serial}/avoidance/reportIngestion endpoint. The Runner reports its own execution state here (COMMANDED acknowledged, EXECUTING, telemetry-confirmed CONFIRMED). The Custodian cross-checks this report against its own independent read of telemetry before writing current_avoidance_state — a Runner self-report alone is never sufficient to mark a maneuver CONFIRMED.Autopilot Runner
Why the Custodian doesn't just trust the Runner's report. COMMAND_ACK alone is not trustworthy on MAVLink — a GUIDED setpoint outside a loaded fence can be silently rejected (Β§9). The Custodian only advances current_avoidance_state to CONFIRMED after independently correlating the Runner's report against the position/attitude telemetry it already receives via the Runner's normal telemetry stream (category E). A report that says EXECUTING but telemetry shows no heading/altitude change writes MISMATCH, not CONFIRMED.

MCP exposure

The MCP server wraps a curated subset of the above as AI-callable tools. The principle is read freely, write narrowly: agents can read drone state, OI status, ownership state, identity, health, and DAA status without restriction; writes are scoped to operationally-safe toggles only.

MCP tool nameMaps toRead/WriteNotes
drone.listGET /dronesRList drones in scope.
drone.getGET /drones/{serial}RFull record.
drone.get_stateGET /drones/{serial}/stateRLive state only.
drone.get_telemetryGET /drones/{serial}/telemetryRTime-range query.
drone.set_oi_automationPUT /drones/{serial}/oi-automationWOperationally safe.
drone.set_oi_distancePUT /drones/{serial}/oi-defaultsWNumeric clamp applied.
drone.set_adsb_surrogatePUT /drones/{serial}/adsb-surrogateWOperationally safe.
drone.set_ownershipPUT /drones/{serial}/ownershipWPermission-scoped.
drone.regenerate_oiPOST /drones/{serial}/oi/regenerateWOperationally safe.
drone.get_healthGET /drones/{serial}/healthRUsed heavily by OCC AI.
drone.get_default_pilotGET /drones/{serial}/default-pilotRPrefill for mission creation.
drone.set_default_pilotPUT /drones/{serial}/default-pilotWPermission-scoped; validates against FLYsafe.software.
drone.get_separation_defaultsGET /drones/{serial}/separation-defaultsRNMAC / SWC (crewed/uncrewed × horizontal/vertical) / EVAL in metres.
drone.set_separation_defaultsPUT /drones/{serial}/separation-defaultsWServer-side clamp enforces nmac < swc < eval.
drone.get_data_sourceGET /drones/{serial}/data-sourceRSource-vigilance summary. High value for OCC AI.
drone.get_data_source_historyGET /drones/{serial}/data-source/historyRPost-flight connectivity review; feeds Compliance AI.
drone.get_volumesGET /drones/{serial}/volumesRFV and CV buffer defaults.
drone.set_volumesPUT /drones/{serial}/volumesWServer-side clamp enforces CV ≥ FV.
drone.get_current_flightGET /drones/{serial}/current-flightRCurrent mission state.
drone.get_current_oiGET /drones/{serial}/current-oiRCurrent OI summary.
drone.get_submitted_volumeGET /drones/{serial}/oi/currentRThe polygon actually sent to DSS. Heavy payload.
drone.get_flight_volume_geometryGET /drones/{serial}/oi/current/flight-volumeRFV polygon alone.
drone.get_contingency_volume_geometryGET /drones/{serial}/oi/current/contingency-volumeRCV polygon alone.
drone.get_waypoint_bufferGET /drones/{serial}/waypoint-bufferRRadius in metres for waypoint arrival detection.
drone.set_waypoint_bufferPUT /drones/{serial}/waypoint-bufferWServer-side clamp: 5–500 m.
drone.get_daa_statusGET /drones/{serial}/daa-statusRHigh value for OCC AI β€” answers “is this drone's DAA coverage trustworthy right now?” v2.0
drone.get_avoidance_historyGET /drones/{serial}/avoidance/historyRPost-flight avoidance review; feeds Compliance AI. v2.0

Operations not on this list (retire, identity registration, mission binding writes, adapter-config changes) are deliberately not exposed via MCP. POST /drones/{serial}/avoidance/report is likewise excluded — it is a machine-to-machine Runner-to-Custodian channel, not an operator or agent action.

Authentication. All API calls carry a JWT issued by the tenant’s Keycloak realm. The JWT identifies the caller (operator, Autopilot Runner session, MCP agent) and is the basis for both authorization and audit. The Runner uses the operator’s session token (MAVLink Runner, embedded in Mission Planner) or its own service session (DJI Runner, Chrome GCS Plugin); MCP agents use per-agent service accounts with scoped permissions.

6. Identity Model — Four-Tier Layout

Unchanged from v1.13. FLYsafe.live carries a drone's identity across four systems; mixing them up breaks the binding pipeline.

SystemIdentity KeyHoldsPurpose
FLYsafe.software DLBdlb_guidAuthoritative drone inventorySystem of record for inventory
tenant.drones + dlb_drones β†’ v_dtwin(tenant_id, serial_number)RTM-managed config + live state + DAA status + dlb_guid FKThe dTwin lives here
Keycloak.ΞΈ.Drone<tenant>-<serial>Drone-as-user authenticationAuthentication only
AADMS(tenant, serial) + callsignCross-tenant identity lookupCross-tenant routing
Serial Number — primary binding key Callsign — ADS-B identifier (per-mission) ICAO 24-bit Hex — dynamic session address TC Registration — regulatory display

7. FLYplan Workflow

Unchanged from v1.13 Β§7 β€” the automated creation and maintenance of Operational Intents. See that document for the full seven-step table (onboarding, pilot-granted OI Automation authority, mission publish, telemetry-driven state advance, OI regeneration, operator review, mission completion).

8. Surveillance & Ownership Tagging

Unchanged from v1.13 Β§8. Flight_Live consults each drone's ownership_flag_enabled (read from the Custodian API) to tag incoming surveillance records as own-aircraft or external. Records are tagged, not filtered, at ingest β€” each downstream consumer applies its own filtering policy. See that document for the full decision record.

9. DAA Execution Model — RTM Core, Maneuver Selection & Protocol Behavior

This section is folded in from FLYsafe Architecture β€” DAA, Custodian & Runner Strategy, grounded in the am-flyrtm-blender repository (a Flight Blender / InterUSS derivative). It is what RTM Core actually computes, and what the Autopilot Runner does with it.

How RTM Core chooses a maneuver β€” candidate generation & forward-simulated scoring

DefaultAvoidanceEngine.compute_maneuver() doesn't apply a fixed rule — it generates several candidate maneuvers and predicts the outcome of each before picking a winner. This is what makes it rich avoidance.

1
Generate candidates
Descend, climb, right turn, left turn, and a combined right-turn-plus-descend β€” each clamped to the airframe's envelope and the time available before CPA.
2
Forward-simulate each candidate
The intruder's projected position is reconstructed from stored bearing/range. Each candidate is re-run through the same trajectory sampler used for detection, producing a predicted post-maneuver minimum separation.
3
Apply GPS-accuracy safety margin
Predicted separation is reduced by configured GPS accuracy figures before scoring β€” the engine never credits a maneuver with more clearance than the position source can guarantee.
4
Ground-risk filter every candidate
Each candidate's projected path is checked against the active geofence set. Any candidate entering a geofence is penalised to near-zero β€” the maneuver the Runner receives has already been checked.
5
Select & assess feasibility
The candidate with the best predicted separation wins. The engine reports whether it's actually feasible β€” flagged insufficient_time, ground_risk_conflict, or envelope_limit when it isn't.
What RTM Core hands off, precisely Not a waypoint or lat/lon target β€” a maneuver: heading change (degrees), target vertical rate (m/s), duration (seconds), predicted resulting separation, an urgency tag, and a feasibility verdict. The Autopilot Runner translates the maneuver into protocol-native commands; it never re-derives where to go, and the Custodian never re-derives whether the maneuver was right β€” only whether it happened.

Three-tier hybrid avoidance — onboard, local injection & cloud

The Runner isn't only a translator for RTM Core's commands β€” it's also a local sensor-aggregation and injection layer that keeps functioning when RTM Core isn't reachable at all.

TierDJIMAVLink / ArduPilotRequires RTM Core link?Who decides
1 β€” Hardware floor Native onboard vision/infrared obstacle detection. Universal, always on. Optional Only present if the airframe has physical proximity sensors wired to ArduPilot's PRX parameters. No Flight-controller firmware β€” physical-object stopgap only
2 β€” Local sensor injection Runner aggregates whatever's wired to the companion computer and synthesizes a sensor-contact injection feeding onboard avoidance logic locally. Runner synthesizes an ADSB_VEHICLE message from aggregated sensor data and injects it into the MAVLink stream; AP_Avoidance reacts via its tuned AVD_* parameters. No β€” this is the no-connectivity fallback Onboard avoidance logic (DJI) / AP_Avoidance (ArduPilot), reacting to Runner-fed sensor data
3 β€” Cloud DAA RTM Core's forward-simulated, geofence-checked maneuver, delivered over the DRC channel (DJI) or streamed Guided Mode setpoints (MAVLink). Definitive when available. Yes RTM Core (DefaultAvoidanceEngine)

Autopilot avoidance paths — ADS-B injection vs. Guided Mode

On the MAVLink side, the Runner has two mechanisms available to cause an avoidance maneuver. Which one runs is an operator-configurable deployment setting β€” FLYsafe's default deployment uses Guided Mode.

Path A β€” ADS-B Injection AlternativePath B β€” Guided Mode FLYsafe Default
MechanismRunner synthesizes an ADSB_VEHICLE message and injects it as a sensor contact.Runner receives RTM Core's computed maneuver and streams SET_POSITION_TARGET_GLOBAL_INT at heartbeat rate.
Who decidesArduPilot's own AP_Avoidance, using its tuned AVD_* parameters.RTM Core β€” already checked against ground risk and geofences.
Known riskDepends entirely on ArduPilot's own tuning being correct.GUIDED setpoints outside a loaded fence can be silently rejected β€” COMMAND_ACK alone is not trustworthy; telemetry confirmation is mandatory (see Β§5K).

MAVLink vs. DJI β€” automatic entry/exit

Both protocol implementations follow the same shape: automatic entry, automatic exit β€” no manual confirmation gate by default, matching the precedent already set by UAvionix Casia.

StageMAVLink / ArduPilotDJI / Cloud API DRC Channel
Automatic entryRunner commands GUIDED mode directly.Cloud sends cloud_control_auth_request; aircraft reports is_cloud_control_auth = true.
Command deliveryStreamed SET_POSITION_TARGET_GLOBAL_INT at heartbeat rate.Joystick-style near-real-time commands over the DRC live-control MQTT channel.
Operator visibilityRunner sends STATUSTEXT into the Mission Planner messages panel.FlightHub 2 control-status flags only β€” Gap no free-text HUD equivalent, closed by the planned Chrome extension.
Automatic exitOnce RTM Core signals the conflict cleared, Runner commands mode back to AUTO.Runner automatically releases DRC authority; DJI Dock's Resume Flight From Breakpoint resumes cleanly.

IP boundary β€” what's stock ArduPilot vs. what's FLYsafe's own

ArduPilot is the muscle β€” dumb and reactive, doing exactly what any operator's ArduPilot does. FLYsafe's differentiation is the orchestration layer above it: RTM Core's DAA reasoning, delivery-and-trust verification through the Custodian, and the Runner's translation-and-confirmation logic that turns a cloud-level “avoid this” into a reliable, provably-executed action on a stock autopilot — without modifying the autopilot itself.

Operator visibility layer

The Mission Planner Plugin (already built, Tim Tuxworth) injects STATUSTEXT-style DAA status directly into Mission Planner's messages panel. The planned FLYsafe.live RTM Chrome Extension (Maykon Ferrero) closes the equivalent gap for any browser-based GCS β€” augmenting the web page UI directly rather than integrating with a single vendor's API, so the same pattern extends to future web GCS platforms without rework.

Full detail lives in the DAA strategy document. The complete RTM Core pipeline (conflict detection, alert lifecycle, surveillance service performance vs. coverage geometry), all six native FigJam diagram recreations, and the open engineering questions specific to DAA (AMQP topology, maneuver-translation ownership, fence upload, DJI DRC implementation) live in FLYsafe Architecture β€” DAA, Custodian & Runner Strategy. This section summarizes the parts that shape the Custodian/Runner contract described in Β§2/Β§3/Β§5K above.

10. Lifecycle & Operational Model

All v1.13 lifecycle events (drone added in DLB, Runner installed, Custodian boots, telemetry arrives, OI automation granted, source degrades, drone armed/airborne, OI accepted, mission completes, drone retired) carry forward unchanged β€” see v1.13 Β§9. Two DAA-specific events are new in v2.0:

Lifecycle EventWhat Happens
Conflict detected, avoidance command issued RTM Core publishes an avoidance_maneuver message to the flight declaration's fanout exchange. Custodian and Runner each receive their own copy. Custodian writes current_avoidance_state = COMMANDED, current_avoidance_maneuver_id. Runner begins execution immediately β€” no wait on the Custodian.
Custodian verifies Runner execution Runner reports its own state via POST /drones/{serial}/avoidance/report; Custodian cross-checks against independently-observed telemetry. Match → current_avoidance_state = CONFIRMED, daa_status stays/returns to FULL. Mismatch → MISMATCH, daa_status = DEGRADED or NO_QUALIFIED_SURVEILLANCE, OCC alerted. Conflict clears → CLEARED.

11. Open Questions & Next Steps

Tracked across the wiki subpages under TECH-002/FSL RTTP. Carried forward from both source documents.

πŸ“ Reference — Polygon Generation API Requirements (Owner: JC)

Β§4 established that three geometries — Flight Volume, Contingency Volume, and the Submitted Volume — are stored in flights_binding and exposed via the Β§5A endpoints. This section specifies the API that actually computes those three polygons from a predicted trajectory and the drone's FV/CV buffer defaults. It does not yet exist; JC is building it. Everything below is the requirements contract for that work — not a description of a shipped API. Unchanged from v1.13.

Where this fits

Flink’s OI Automation job (FLYplan step 5, Β§7) predicts the trajectory and calls this API to get the three polygons back. Flink then submits the Submitted Volume to FLYrtm DSS and, on acceptance, is responsible for persisting all three geometries to flights_binding. This API’s job is purely geometric — trajectory + buffers in, three polygons out. It does not talk to DSS and does not write to Postgres itself.

EndpointPurposeCaller
POST/drones/{serial}/oi-geometry/computeCompute the Flight Volume, Contingency Volume, and Submitted Volume polygons for one predicted trajectory. Stateless — does not write to flights_binding; the caller (Flink) persists the result there after DSS submission.Flink (OI Automation job)

Request contract

{
  "tenant_id": "airmarket",
  "serial_number": "1581F5BBB1F2A",
  "trajectory": [                              // predicted path, ordered, ≥ 2 points
    { "lat": 53.5461, "lon": -113.4938, "alt_msl_m": 720.4, "t": "2026-09-05T14:22:00Z" },
    { "lat": 53.5480, "lon": -113.4901, "alt_msl_m": 725.1, "t": "2026-09-05T14:23:30Z" }
    // ...
  ],
  "fv_buffer_m": 56.0,                          // horizontal only
  "cv_buffer_m": 150.0
}

Response contract

{
  "flight_volume":      { "type": "Polygon", "coordinates": [ /* GeoJSON PolygonZ */ ] },
  "contingency_volume": { "type": "Polygon", "coordinates": [ /* GeoJSON PolygonZ */ ] },
  "submitted_volume":   { "type": "Polygon", "coordinates": [ /* GeoJSON PolygonZ — see open question below */ ] },
  "computed_at": "2026-09-05T14:22:18.600Z"
}

Functional requirements

RequirementDetail
FV polygonHorizontal only: the trajectory linestring expanded outward by fv_buffer_m. The polygon keeps the trajectory’s own altitude at each vertex (PolygonZ) — there is no vertical buffer expansion. Vertical separation from other traffic is handled by the NMAC/SWC crewed/uncrewed minima (Β§5I), not by FV/CV.
CV polygonSame construction as FV, using cv_buffer_m instead. Must fully contain the FV polygon horizontally — the API should validate this and reject (422) any input where cv_buffer_m < fv_buffer_m.
Submitted VolumeThe single polygon that is the combination of FV and CV, structured however FLYrtm DSS’s OI submission format requires it (see open question below). This is the only one of the three actually transmitted to DSS.
DeterminismSame trajectory + same buffers must always produce the same three polygons, byte-for-byte. Regeneration diffing (Β§7 step 5) depends on this.
LatencyCalled on every OI regeneration tick, so this must return well within Flink’s regeneration cadence. Target: p95 < 200 ms per call.
No side effectsPurely a compute step — must not write to flights_binding, must not call DSS, must not read/write any drone state. Keeps the geometry math independently testable.
Open implementation questions for JC to resolve before build.