🚁 FLYsafe.live - v1 - Aircraft Custodian & dTwin Strategy

An Aircraft Custodian maintains the dTwin for each drone — receiving data via GCS Plugins, persisting state to tenant.drones Postgres, exposing it through APIs that the frontend uses today and MCP agents will use tomorrow
Version 1.13 · Conspicuity Columns, DAA Rules Table, oi_geometry → flights_binding

🎯 Core Thesis

One Aircraft Custodian per drone, per tenant. The Custodian maintains the dTwin. State lives in tenant.drones Postgres. Everything else — the frontend, AI agents, surveillance services — reads and writes the dTwin through APIs.

Naming note: the Aircraft Custodian is the entity we previously called the Aircraft Agent. Same concept, new name — chosen because the entity does more than "agent" suggests: it actively curates and maintains the dTwin as a custodian of record, not merely a passive translator of telemetry.

The dTwin is no longer treated as “a retained MQTT topic” first and a database row second. It is the canonical record of an aircraft’s current state in tenant.rtm-app-db.drones, maintained by the Aircraft Custodian via API calls, and surfaced to consumers (frontend app, AI agents, surveillance, evidence) through a stable REST API. MCP wraps that same API so future agents interact with the state through the same contract the frontend uses today.

🎯 Solution Concept

The Aircraft Custodian is split into two Docker microservices: Custodian Software (one process per drone) and Custodian APIs (the shared REST layer). Between them sits RTTP — Real Time Telemetry Processing, the central telemetry hub. All GCS Plugins and the Cloud Broker stream telemetry into RTTP; RTTP (encompassing Flink event processing) normalises and routes data to the Custodian APIs, which write the dTwin to tenant.drones. The same API contract is consumed by the browser UI, MCP agents, and the Flight_Live microservice.

Aircraft Custodian Solution Concept GCS plugins → RTTP → Custodian APIs → tenant.drones ⋅ RTTP encompasses Flink ⋅ Flight_Live reads AADMS.drones 1 AIRCRAFT & GCS 2 GCS PLUGINS & BROKERS 3 CUSTODIAN SW 4 RTTP 5 CUSTODIAN APIs 6 STATE & DATA STORES 🛩️ MAVLink Aircraft ArduPilot · PX4 · Sentaeros Flown via Mission Planner (desktop GCS, MAVLink v2) Operator workstation 🎮 DJI / Web GCS Matrice · Mavic Enterprise Flown via DJI FlightHub 2 (browser-based GCS) Operator browser tab 🔌 Mission Planner Plugin Embedded inside Mission Planner; forwards telemetry & alerts. Taps MAVLink stream from autopilot → streams telemetry to RTTP 🧩 Chrome GCS Plugin Browser extension for web GCS (DJI FH2, others) Reads telemetry from GCS DOM / API → streams telemetry to RTTP ☁️ Cloud Broker 3rd-party GCS adapter microservice Brokers telemetry from external GCS platforms (Auterion, Skydio, etc.) Cloud-hosted ⋅ streams to RTTP 🪞 Custodian SW 🐳 Docker Microservice One per drone, per tenant 🪞 Representation Identity in tenant.drones 📡 Streaming Honest telemetry / failure 🎯 Currency Maintains canonical state Sends telemetry data → RTTP Interacts with Custodian APIs to maintain the dTwin Telemetry → RTTP dTwin writes → Custodian APIs Both outputs run concurrently per telemetry event ⚡ RTTP Real Time Telemetry Processing Ingest · Validate · Normalise · Route Receives telemetry from: Mission Planner Plugin · Chrome GCS Plugin Cloud Broker · Custodian SW Outputs processed telemetry to: Custodian APIs · Evidence & Storage Provides updates to Custodian API interface on all telemetry events ⋅ no direct DB writes all state flows through Custodian APIs ⚙️ Flink Processing Event detection · OI evaluation CIFIB gating · surveillance tagging Streams to Evidence & Storage No raw telemetry reaches tenant.drones directly Ownership flag sourced from Custodian APIs Flight_Live reads AADMS.drones ⋅ calls Custodian APIs 🌐 Custodian REST APIs 🐳 Docker Microservice FLYplan · OI Automation · Defaults ADS-B provisioning · Identity · State Receives: RTTP updates · Custodian SW writes Serves: FLYsafe.live · MCP · Flight_Live Writes: tenant.drones (canonical dTwin state) 📊 tenant.drones PK: (tenant_id, serial_number) DLB-sourced + RTM-managed columns dTwin state: identity · OI · ADS-B rtm-app-db Postgres Custodian keeps this row current & honest 🗄️ AADMS.drones Aircraft registration & identity data Source of truth for aircraft metadata ✈️ Flight_Live Live flight processing microservice Reads aircraft data from AADMS.drones Correlates with live telemetry state Input: AADMS.drones · Calls: Custodian APIs 7 CONSUMERS 🗺️ FLYsafe.live RTM Map / OCC (JC) Frontend application Reads drone state & live position from APIs Writes operator changes (OI toggle, ownership) Uses the same APIs as Custodian SW + MCP 🤖 MCP Server → AI Agents (future) Same APIs, exposed as MCP tools Aircraft AI · OCC AI · Compliance AI Permission-scoped; read & safe-write only No new contract — same APIs as Custodian SW 📋 Evidence & Storage Postgres history · InfluxDB telemetry CAR 922.08/09/10 evidence logs GUTMA GeoJSON output for compliance Receives validated telemetry stream from RTTP KEY PROPERTIES OF THIS DESIGN • All GCS plugins and the Cloud Broker stream telemetry into RTTP. RTTP (encompassing Flink event processing) validates, normalises, and forwards to Custodian APIs and Evidence & Storage. No raw telemetry hits the DB. • Custodian SW and Custodian APIs are separate Docker microservices. SW feeds telemetry to RTTP and writes the dTwin via APIs. APIs write tenant.drones. One contract: SW, browser UI, MCP agents, and Flight_Live all use the same endpoints. • Flight_Live reads from AADMS.drones for aircraft identity, then calls Custodian APIs to correlate live-flight records with the dTwin. The flight ownership flag originates in the dTwin, not raw telemetry.

The four pillars of the v1.6 design

Custodian maintains the dTwinOne per drone. Honest representation of the aircraft’s current state — that is its job.
GCS Plugins are the data pathMission Planner Plugin for MAVLink fleets; Chrome GCS Plugin for browser-based GCS (DJI FH2 and others). They forward telemetry & alerts to the Custodian.
State lives in PostgresThe dTwin is a row in tenant.drones, kept current by the Custodian. Queryable, joinable, durable — the basis for all downstream consumers.
APIs are the contractCustodian writes state via APIs; frontend reads and writes operator changes through the same APIs; MCP exposes them to AI agents. One contract, three audiences.
What about MQTT? MQTT was the canonical example in v1.4–v1.5 for streaming the dTwin to subscribers. In v1.6 we no longer treat it as the primary state mechanism. State is the database row. MQTT (or any other transport) becomes an optional distribution channel for consumers that want a push feed of changes — not the contract itself. For most consumers, polling or subscribing to the API’s change-notification endpoint is sufficient.

🛠️ Guiding Principles

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

This is the structural view: where drone records originate, how they enter tenant.drones, and where identity propagates. It corresponds directly to the architecture whiteboard from the Jun 15 Daily Technical Review.

Drone Data Setup Flow · How Drones Enter FLYsafe.live From FLYsafe.software DLB through tenant.drones into Keycloak and AADMS · Custodian maintains the dTwin row 🪞 Aircraft Custodian (per drone, per tenant) Receives data from GCS Plugins (§3) · Normalizes · Maintains dTwin state Writes to tenant.drones via Custodian APIs (§5) { Representation · Streaming · Currency } maintains via Custodian APIs 📚 FLYsafe.software DLB.Drones Drones • Core RID fields • Serial · Brand · Model • Legal ID (TC reg) Import Process (Steve Smith) - DEV - PROD 📊 tenant.drones (T.Drones) in tenant.rtm-app-db (one per tenant) GUID SN (PK) Callsign …+ more dlb_guid • Default parameters for RTM needs adapter_type · publish_rate · OI defaults • Metadata for RTM purposes adsb config · ownership flag · OI automation • DLB-sourced fields (sync, read-only) brand · model · drone_name · legal_id 🌐 AADMS."Schema".tenant.Drones Cross-tenant routing · callsign → tenant Tenant SN Callsign Guid ID Sits above tenants; routes inbound ADS-B records (carry callsign only) to the right tenant DB. DEV and PROD environments use separate AADMS schemas. PROD DEV cross-tenant publish 🔐 Keycloak.θ.Drone Drone-as-user authentication only SN Guid Tenant ID User ID format: <tenant>-<SN> Auth only — no drone metadata. (Stephen: “What you need to give it access”) identity sync Example drone-as-user IDs airmarket-TC0420080 sait-AC0S-69 sait-CTC0422080 Format: <tenant>-<SN> 🗺️ FSL RTM Map Frontend (JC) JC · API consumer • Own RTM-managed fields only • Pulls DLB-sourced as read-only • Add-on fields MOD ONLY Updates via Custodian APIs read & write via API LEGEND Import / sync path (DEV & PROD) Custodian maintains dTwin in tenant.drones Frontend API read/write · AADMS cross-tenant publish Identity propagation to Keycloak / AADMS

What this diagram says

2. The Custodian Role — Three Obligations

An Aircraft Custodian is the entity inside FLYsafe.live RTM responsible for representing one specific drone. There is exactly one Custodian per drone per tenant. The Custodian carries three formal obligations, each independently observable through the state it maintains in tenant.drones.

🪞 Representation

Single point of drone identity in RTM. The Custodian maintains the canonical row in tenant.drones. All consumers — Flink, RTM Map, AI agents, evidence services — read identity through the same APIs.

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

📡 Streaming & Source Vigilance

Honest telemetry flow with explicit failure declaration. The Custodian identifies which mechanism is delivering data for its drone, monitors the rate that source is producing, and updates its health state the moment the rate drops or latency exceeds the drone’s expected envelope (§3.6). When the source disconnects, the Custodian sets adapter_health.connected = false and data_source_status = DISCONNECTED.

Observable via: adapter_health.connected, last_publish_ago_ms, data_source_status, data_source_rate_hz, data_source_latency_ms_p95.

🎯 Currency

Maintains canonical drone state — position, attitude, battery, mission binding, ADS-B status, Remote ID, ownership flag, OI defaults. State changes flow from the GCS Plugin into the database via Custodian APIs.

Observable via: timestamps.source_ts, timestamps.ingested_at, updated_at in tenant.drones.

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. A Custodian has obligations it can succeed or fail at, observably. The implementation is still a small server-side component per drone — what changes is the framing of what it does: it keeps the database row honest.

3. GCS Plugins — How Telemetry Enters the System

The data path into the Aircraft Custodian is a plugin embedded inside the operator’s GCS — not a separate edge container the operator has to install and manage. Two plugin form factors cover the bulk of our drone fleet:

🔌 Mission Planner Plugin  (for MAVLink fleets: ArduPilot, PX4, Sentaeros)

Owner: Tim Tuxworth Host: Mission Planner desktop GCS Protocol: MAVLink v2 Status: in active development

A native plugin loaded inside Mission Planner. Taps the MAVLink stream the operator is already receiving from the autopilot, normalizes selected messages (GLOBAL_POSITION_INT, ATTITUDE, BATTERY_STATUS, MISSION_CURRENT, ADSB_VEHICLE, etc.), and forwards them to the Custodian API as HTTPS POSTs at a configurable rate.

Why a plugin and not a separate container: Mission Planner is already running on the operator’s workstation during every flight. We piggyback on the existing tooling and credentials. No second installer, no separate auth, no extra network hop.

What it sends: position, attitude, velocity, battery, GPS quality, flight mode, mission progress, ADS-B observations, link health, alerts (mode changes, RTL, failsafe).

🧩 Chrome GCS Plugin  (for browser-based GCS: DJI FlightHub 2, similar web GCS)

Owner: Maykon Ferrero Host: Chromium browser extension Protocol: GCS web APIs / DOM Status: design phase

A Chromium browser extension that activates when the operator has a web GCS open (DJI FH2 today, others in future). It reads telemetry and alerts from the GCS’s exposed web APIs (or as a fallback, the GCS’s observable DOM/WebSocket traffic), normalizes it, and forwards to the Custodian API as HTTPS POSTs.

Why a browser extension: DJI FH2 is a web app. The operator is already authenticated and connected to the live MQTT/REST surface DJI exposes. The plugin rides along inside the same browser session — we don’t need to set up a separate cloud microservice with DJI credentials per drone.

What it sends: position, mode, battery, mission state, payload status, gimbal state, alerts, ADS-B observations (where exposed by the GCS).

Extensibility. The plugin 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 plugin built for it. DroneSense, Skydio Cloud, and other vendor GCSs are candidates as fleets grow. The Custodian API contract stays the same; only the plugin’s data-extraction code differs.

Why this is materially different from v1.4–v1.5

Prior versions described the integration as a Python Aircraft Agent container per drone, with adapters for MAVLink/DJI/DroneSense/ADS-B. v1.6 moves the integration point into the operator’s GCS. The Custodian itself becomes a server-side component that takes API input from the plugin, normalizes/persists state, and exposes APIs to consumers. Benefits:

v1.4–v1.5 (edge agent)v1.6 (GCS plugin)
Container deployed per drone (Docker image with adapters) Plugin loaded inside Mission Planner / Chromium where operator already works
Operator must install/manage edge runtime Plugin distributed via Mission Planner’s plugin loader / Chrome Web Store
Separate auth for edge agent Operator’s existing GCS session carries the identity
Second network hop — MAVLink → agent → MQTT broker One hop — GCS → HTTPS POST to Custodian API
State lived in retained MQTT topics State lives in tenant.drones Postgres; APIs are the contract

3.5 Centralized Configuration Management via Custodian APIs

The Aircraft 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. This is what makes FLYsafe.live RTM a centrally-managed fleet platform rather than a collection of one-off integrations.

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 Custodian 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.

Per-protocol configuration the Custodian manages

DJI FlightHub 2

For: DJI fleets managed via FH2 web GCS

Configuration: FH2 workspace ID, device alias, MQTT broker endpoint, REST API base URL, telemetry topic filter, OSD message subscription set

Served to the Chrome GCS Plugin 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, telemetry stream selection, 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 enabled, ADS-B Out enabled, surrogate mode, expected callsign, ICAO hex mode (dynamic / ADDRt), TIS-B rebroadcast participation (tisb_rebroadcast_enabled — when set, the Custodian rebroadcasts this aircraft’s own telemetry into the TIS-B feed so ground-based ADS-B receivers can see it)

Drives both CIFIB upstream gating and Flight_Live ownership tagging

Why this is the right model. Every fleet operator on FLYsafe.live has drones from multiple vendors. Without centralized config management, the operator would need to maintain connection settings in four different places (Mission Planner config files, DJI FH2 dashboard, DJI Cloud admin console, separate ADS-B provisioning tool). With the Custodian model, all four live in one Postgres table, exposed through one API, edited in one UI. The operator sees one fleet, regardless of how each drone gets its data into the system.
Vendor credentials, not in the configuration row. The configuration document references secrets (DJI app credentials, FH2 service tokens, MAVLink TLS certs) 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. The Custodian’s Streaming obligation (§2) is discharged by continuously monitoring the source and surfacing its health through the state that lives in tenant.drones.

What the Custodian tracks per drone

For each drone, the Custodian identifies and records:

Status transitions

StatusTriggerWhat downstream consumers do
HEALTHY Rate 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.
DEGRADED Rate is below expected but source is still delivering, or p95 latency has exceeded threshold but data still arriving. Map shows a warning halo around the icon. OI Automation continues but flags OIs generated during degradation. OCC is notified.
STALE No message received in the last N × the expected interval (default N = 5). Map suppresses live position; renders last-known position with a stale marker. OI Automation pauses regeneration. Alert raised.
DISCONNECTED Source has explicitly disconnected (plugin closed, webhook subscription lost, Cloud Broker session dropped, ADS-B feed dropped). Same as STALE plus the map badge changes to indicate the drone is 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, not per-frame, so a single delayed frame doesn’t look like a degradation.
Multi-source drones. A drone can have more than one source active simultaneously (e.g. Mission Planner Plugin providing MAVLink telemetry and ADS-B feed providing surrogate position). The composite status recorded in data_source_status is the roll-up; per-source detail is available from RTTP/Flink’s own source-tracking state and surfaced at GET /drones/{serial}/data-source/history for post-flight review (§5J).

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 reads through the API surface that sits on top of this table.

DLB Field Cross-Reference

The table below maps every field the FLYsafe.software DLB exposes for a drone to its role in the Aircraft 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.

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 Pixhawk Cube Stored / informational Flight controller model. Useful for adapter type selection guidance (Pixhawk → MAVLink).
firmware_version 4.0.7 Stored / informational Autopilot firmware version at time of DLB registration.
propulsion_type ELECTRIC Stored / informational Propulsion category (ELECTRIC, ICE, HYBRID).
payload_capacity 5 Stored / informational Maximum rated payload in kg.
weight 5kg Stored / informational Empty aircraft weight.
purchase_date 2021-03-01T00:00:00.000Z Stored / informational Aircraft acquisition / commissioning date.
max_flight_time 0 Stored / informational Rated endurance in minutes (0 = not set in DLB).
insurable_value 0 Stored / informational Insurance / replacement value in local currency.
color black Stored / informational Aircraft airframe colour.
company_guid 9D1F94A9-97B3-… Stored / informational DLB company / tenant identifier for reconciliation.
user_guid FE9F94FE-3E9C-… Stored / informational DLB user who registered the drone.
controller_serial_number (empty) Stored / informational Primary RC controller serial number.
controller_serial_number2 (empty) Stored / informational Secondary RC controller serial number.
flight_controller_serial_number (empty) Stored / informational Flight controller board serial number.
tech_number (empty) Stored / informational Internal technical reference number.
notes (empty) Stored / informational Free-form notes field from DLB.
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 and live telemetry 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, making forward sync and any future column additions straightforward.

-- 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 Aircraft Custodian. Holds only RTM-specific data — configuration, live state, and the link key (dlb_guid) back to drones.dlb_drones. All DLB identity fields (name, brand, model, callsign, TC registration, etc.) live exclusively in dlb_drones and are accessed via the v_dtwin view. Speed defaults are seeded from dlb_drones on first insert and may be overridden by the operator.

-- /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 §5A (v1.10)
  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 (v1.8; supersedes the v1.6–v1.7 scalar
  -- flight_volume_m / contingency_vol_m fields. Horizontal only as of v1.12 — 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 OI Configuration — New Required Fields below)
  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 (v1.10) — radius within which a mission waypoint is considered reached.
  -- Respected by Flink flight-monitoring jobs when advancing current_waypoint_index.
  waypoint_buffer_m                 float8 DEFAULT 50.0 NOT NULL,    -- valid 5–500

  -- Default pilot binding (v1.7) — 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 (v1.7; split into crewed/uncrewed × horizontal/vertical in v1.12) —
  -- per-drone conflict-evaluation minima, metres. Crewed = manned traffic; uncrewed = other drones.
  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 (v1.13) — provisioning detail behind the flags above.
  -- ec_callsign and ec_tc_reg are NOT columns here: they are dlb_drones.inventory_number
  -- and dlb_drones.identification_number respectively, already exposed as Callsign /
  -- TC_Registration in v_dtwin. The RTM Map's "ec_*" fields are just local aliases for those.
  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
  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, v1.13) — 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

  -- Ownership flag (v1.5: replaces ghost extraction; see §8)
  ownership_flag_enabled            bool DEFAULT false NOT NULL,

  -- Data source monitoring (v1.7) — the Custodian’s vigilance obligation; see §3.6
  data_source_mechanism             text NULL,               -- mp_plugin | chrome_fh2 | 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 (v1.9) — 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 (v1.9) — 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 GCS plugin 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);

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.

-- drones.v_dtwin · /tenant/rtm-app-db/schema/v_dtwin.sql
-- dTwin view: joins drones.drones (RTM state) with drones.dlb_drones (DLB identity).
-- Use this view for any consumer that needs the full drone record.
CREATE OR REPLACE VIEW drones.v_dtwin AS
SELECT
  -- Core identity
  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",   -- TC / regulatory registration
  dlb.inventory_number              AS "Callsign",           -- operator-assigned ADS-B 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 (RTM-managed)
  d.adapter_type,
  d.adapter_config,
  d.publish_rate_hz,

  -- OI defaults & FLYplan automation
  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,

  -- Pilot & separation defaults (v1.7)
  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,

  -- ADS-B provisioning
  d.adsb_in_enabled,
  d.adsb_out_enabled,
  d.adsb_surrogate_enabled,
  d.tisb_rebroadcast_enabled,

  -- Conspicuity / ADS-B detail (v1.13)
  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,

  -- Observed ADS-B state (v1.13)
  d.adsb_observed_broadcasting,
  d.adsb_observed_last_icao_hex,
  d.adsb_observed_last_seen_by_cifib_at,

  d.ownership_flag_enabled,

  -- Data source monitoring (v1.7) — 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 (v1.9)
  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. In the current architecture this is 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) so the record can be routed to the correct tenant pipeline. Refreshed after each Import sync run.

-- aadms.drones · cross-tenant drone lookup
-- Materialized view aggregating all tenant drones.drones rows (joined to dlb_drones for callsign).
-- Refreshed after every Import sync. Used by ADS-B routing to resolve callsign → (tenant, serial).
CREATE SCHEMA IF NOT EXISTS aadms;

CREATE MATERIALIZED VIEW aadms.drones AS
  -- One UNION ALL block per tenant schema; extend as tenants are added.
  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. The materialized view is refreshed with REFRESH MATERIALIZED VIEW CONCURRENTLY aadms.drones at the end of each Import sync run. The CONCURRENTLY option keeps the view queryable during refresh (requires the unique index on callsign). DEV and PROD use separate AADMS schemas so refreshes are independent.

Where OI geometry is actually stored (v1.13)

fv_buffer_m/cv_buffer_m on drones.drones are configuration — a single horizontal buffer distance used to compute a volume. They are not the volume itself. Every time the Flink OI Automation job generates or regenerates an OI, it computes three distinct geometries (Flight Volume, Contingency Volume, Submitted Volume) from those buffers plus the predicted trajectory.

(v1.13) There is no drones.oi_geometry table. Earlier versions of this doc (v1.11–v1.12) specified a dedicated drones.oi_geometry table for these three geometries. That table was never built. The three geometries for current and upcoming flights are instead persisted in the existing flights_binding table — the same table already used to bind drones to their current/upcoming flights, outside the drones schema. This doc does not restate flights_binding's own column definitions; see its owning schema/doc for the full DDL. Wherever this doc previously said “persisted in drones.oi_geometry,” read “persisted in flights_binding” instead — the conceptual point (all three geometries are stored, not just the buffer inputs; submitted_volume is stored separately rather than recomputed) still holds, only the storage location changed.

Schema: DAA Rules Store (drones.daa_rules)

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

-- 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 and drones.drones are not alternatives — they are two complementary layers of the same pipeline, each with a distinct owner and purpose. 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, and the dlb_guid foreign key that links it to dlb_drones. DLB identity data is never duplicated into drones.drones; consumers that need the full picture use the v_dtwin view.

Three distinct processes write to these tables at different times. Each process owns specific columns and must not write to columns owned by another. Breaking that boundary — for example, the Import process overwriting operator OI configuration, or the Custodian writing into dlb_drones — corrupts the data contract.

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 — the single authoritative copy of DLB data. All columns are overwritten on every sync run. Column names are preserved exactly as DLB delivers them. 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. These columns have no DLB equivalent and are invisible to the Import process.
3 GCS Plugin → drones.drones The Custodian updates the live-state columns on every telemetry frame received from the GCS Plugin: position_lat/lon/alt, flight_mode, armed, battery_remaining_pct, adapter_connected, last_telemetry_at. Neither the Import process nor operators write these columns directly.
v_dtwin (read-only) The v_dtwin view joins all three surfaces into a single consistent dTwin record. The Custodian API responses, frontend UI, and MCP tools all read from this view when the full drone record is needed.

What the Import process writes from dlb_drones into drones.drones

dlb_drones column drones.drones column Sync rule Notes
guid dlb_guid Written on first INSERT Foreign key linking drones.drones to its source row in drones.dlb_drones. Immutable after first insert — the link never changes.
serial_number serial_number Written on first INSERT Forms the composite PK (tenant_id, serial_number). Immutable in practice after first insert.
max_horizontal_speed (TEXT) horizontal_speed_ms (FLOAT) Seeded on first INSERT only Cast TEXT → FLOAT during import. Subsequent sync runs skip this column if the row already exists — preserving any value an operator has set via PUT /drones/{serial}/oi-defaults.
max_vertical_speed (TEXT) vertical_speed_ms (FLOAT) Seeded on first INSERT only Same seeding rule as horizontal speed. Operator overrides are preserved across syncs.
All other dlb_drones columns (name, brand, model, callsign, TC reg, status, hardware specs, etc.) Not copied Remain exclusively in drones.dlb_drones. Accessible in full via the v_dtwin view (JOIN on dlb_guid). Not duplicated into drones.drones.
RTM-managed columns (adapter_type, oi_automation_enabled, ADS-B flags, etc.) Import never writes Owned exclusively by the Custodian APIs (process 2). The Import process must exclude these from its upsert so operator configuration is never silently reset.
Live-state columns (position_lat/lon/alt, flight_mode, armed, etc.) Import never writes Owned exclusively by the Custodian’s telemetry ingestion path (process 3). Set to NULL / defaults until the first GCS Plugin frame arrives.
Upsert design constraint. On first encounter the Import process uses INSERT … ON CONFLICT (tenant_id, serial_number) DO NOTHING to create the drones.drones row with dlb_guid, serial_number, and the two speed seeds. All subsequent sync runs update only drones.dlb_drones — they do not touch drones.drones at all. RTM-managed and live-state columns are never in scope for the Import process.

The three field categories

CategoryPrimary storeWriterExamples
DLB identity drones.dlb_drones (verbatim DLB copy) Import script (Steve Smith) — RTM side cannot modify name, brand, model, drone_type, status, identification_number, inventory_number — exposed via v_dtwin with friendly aliases
RTM-managed drones.drones Custodian APIs (operator UI, Custodian itself, MCP agents) adapter_type, publish_rate_hz, oi_automation_enabled, oi_automation_square_m, oi_automation_radius_m, oi_automation_amend_min_trigger_m, waypoint_buffer_m, fv_buffer_m, cv_buffer_m, default_pilot_name, default_pilot_fs_guid, nmac_h_crewed_m/nmac_v_crewed_m/nmac_h_uncrewed_m/nmac_v_uncrewed_m, swc_h_crewed_m/swc_v_crewed_m/swc_h_uncrewed_m/swc_v_uncrewed_m, eval_distance_m, adsb_surrogate_enabled, tisb_rebroadcast_enabled, ownership_flag_enabled, current_mission_id, current_flight_state, current_oi_id, current_oi_state
Live state drones.drones Custodian (server-side) writing as it normalizes plugin POSTs position_lat, position_lon, position_alt, flight_mode, armed, battery_remaining_pct, adapter_connected, last_telemetry_at
One-way DLB sync; cadence TBD. The Import process (Steve Smith) runs hourly or nightly — final cadence is open question DM-3. drones.dlb_drones is overwritten on every sync run. drones.drones is only touched by the Import process on first insert (writing dlb_guid, serial_number, and seeding the two speed defaults). RTM-managed and live-state 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: the join of drones.drones (RTM configuration and live state) with drones.dlb_drones (DLB identity). Consumers don’t need to know which table backs which field; they call the API and get a coherent record sourced from the view.

{
  "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           // see §9
  },
  "pilot": {                                    // v1.7 — default pilot binding
    "default_name":    "Jane Chen",
    "default_fs_guid": "pilot-8f3a2e19-4c1a-4e6b-9c17-2b9e5d3f7a0c"
  },
  "oi": {
    "automation_enabled": true,      // pilot-granted; see §5A (v1.10)
    "default_distance_m": 300,
    "waypoint_buffer_m":  50.0      // v1.10 — radius: drone "reached" waypoint when within
  },
  "volumes": {                                  // v1.8; horizontal-only as of v1.12 — see current_oi.geometry_ref for the actual computed polygons
    "flight_volume":      { "buffer_m": 56.0 },
    "contingency_volume": { "buffer_m": 150.0 }
  },
  "separation": {                               // v1.7; split into crewed/uncrewed × horizontal/vertical in v1.12
    "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": {                              // v1.7 — see §3.6
    "mechanism":      "mp_plugin",
    "rate_hz":        4.0,
    "latency_ms_p50": 180,
    "latency_ms_p95": 420,
    "max_latency_ms": 2000,
    "status":         "HEALTHY",
    "last_seen_at":   "2026-06-16T18:42:18.315Z"
  },
  "current_flight": {                           // v1.9 — the mission this drone is executing right now
    "mission_id": "mission-airmarket-2026061618420",
    "state":      "ACTIVE",             // PLANNED | ACTIVE | COMPLETED | ABORTED
    "window": { "start": "2026-06-16T18:30:00Z", "end": "2026-06-16T19:15:00Z" },
    "waypoints": { "current_index": 5, "total": 12 },
    "progress_pct": 42
  },
  "current_oi": {                               // v1.9 — the active Operational Intent for this drone
    "oi_id":  "oi-airmarket-2026061618421",
    "state":  "ACTIVE",               // DRAFT | SUBMITTED | ACCEPTED | ACTIVE | COMPLETED | REJECTED
    "source": "AUTOMATED",            // AUTOMATED | MANUAL | MISSION_PUBLISH
    "last_regenerated_at": "2026-06-16T18:41:15Z",
    "next_regeneration_at": "2026-06-16T18:41:45Z",
    "geometry_ref": {                           // v1.11 — three distinct stored geometries, not one
      "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"          // FV + CV combined; what was sent to FLYrtm DSS
    }
  },
  "adapter_health": {
    "connected": true,
    "last_publish_ago_ms": 247
  },
  "timestamps": {
    "updated_at": "2026-06-16T18:42:18.523Z",
    "source_ts":  "2026-06-16T18:42:18.491Z"
  }
}

↑ Returned by GET /drones/{serial}. The Custodian's REST API serves this from the tenant.drones row + latest telemetry. There is no longer a canonical “dTwin payload schema” separate from the API response — the response is the schema. Cheaper subset endpoints (/state, /current-flight, /current-oi, /data-source) exist for consumers that don’t want the full payload every time.

What happened to MQTT? MQTT topic distribution of the dTwin (the v1.4–v1.5 retained-topic model) is no longer the primary contract. Consumers that need a push feed of changes can subscribe to a future change-notification stream, but that is an optimization on top of the API — not the system of record. The system of record is the Postgres row.

5. Aircraft Custodian APIs

This is the catalog of API operations the Aircraft Custodian exposes. 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, Custodian, MCP) reads through a view that projects the appropriate columns. Writes go through view triggers that update drones.drones. This gives per-consumer field isolation and stable contracts as the schema evolves.

API surface — by category

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 (v1.10). The pilot is the source of authority for OI Automation on any given drone. The pilot grants that authority explicitly by flipping the OI Automation toggle to on for the drone on the Inventory page of the RTM Map app. Once granted, the Custodian owns the state transitions autonomously — current_flight_state flips from PLANNED to ACTIVE on armed+airborne detection, the Flink OI Automation job regenerates the OI as trajectory changes warrant, and current_oi_* is refreshed on every acceptance from DSS. The pilot can revoke authority at any time by flipping the toggle back to off, at which point the Custodian holds the last-known state and does not auto-regenerate.
What FV and CV mean here (v1.8; horizontal-only as of v1.12). Flight Volume (FV) is the volume the drone is expected to operate within under normal conditions — the trajectory expanded horizontally by the FV buffer. Contingency Volume (CV) is the further-expanded volume the drone may need if a contingency response fires (return-to-launch, failsafe, comm loss). CV always contains FV. fv_buffer_m / cv_buffer_m on drones.drones are per-drone defaults for these buffers and are pulled into every auto-generated OI unless the mission overrides them at publication. FV/CV carry no vertical component — vertical separation from other traffic is the job of the NMAC/SWC crewed/uncrewed minima below, not FV/CV. These fields supersede the flat flight_volume_m / contingency_vol_m scalars used through v1.7, and the v1.8–v1.11 lateral+vertical split.
Three volumes, all stored and shared (v1.11; storage moved to flights_binding in v1.13). FV and CV are not just configuration inputs — each OI generation produces three distinct geometries, and all three are persisted in flights_binding and exposed via the Custodian API: the Flight Volume polygon, the Contingency Volume polygon, and the Submitted Volume — the single specific polygon that is the combination of FV and CV, and the one actually transmitted to the FLYrtm DSS. GET /drones/{serial}/oi/current returns the Submitted Volume; the two component volumes each have their own endpoint below.
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 }. This is the pilot-authority toggle (v1.10) — when enabled, the Custodian and Flink OI Automation job manage the drone’s OI lifecycle autonomously.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. Partial updates supported; ranges enforced server-side.Frontend
GET/drones/{serial}/oi-defaultsRead the current OI defaults.Frontend · MCP · Custodian (boot)
GET/drones/{serial}/volumesRead the drone’s FV and CV buffer defaults (config, not geometry) — horizontal only: { flight_volume: { buffer_m }, contingency_volume: { buffer_m } }.Frontend · MCP · Flink · Custodian (boot)
PUT/drones/{serial}/volumesSet FV and/or CV buffer defaults. Body accepts either or both of fv_buffer_m / cv_buffer_m. Server-side clamp enforces fv_buffer_m ≤ cv_buffer_m, so CV always contains FV.Frontend
GET/drones/{serial}/waypoint-bufferRead the waypoint-arrival buffer in metres (waypoint_buffer_m, default 50 m). Flight-monitoring jobs treat the aircraft as having “reached” a waypoint once it enters this radius.Frontend · MCP · Flink
PUT/drones/{serial}/waypoint-bufferSet the waypoint-arrival buffer. Body: { waypoint_buffer_m: number }. Server-side clamp: 5 m ≤ value ≤ 500 m.Frontend
POST/drones/{serial}/oi/regenerateForce-regenerate the OI now (skip waiting for the next trajectory tick). Useful for testing or after a defaults change. (v1.13) Backend-only — not surfaced as a manual action in the RTM Map GUI; available to MCP/ops tooling.MCP
GET/drones/{serial}/oi/currentFetch the Submitted Volume — the GeoJSON polygon that is the combination of FV and CV and was actually sent to FLYrtm DSS, with active mission reference. (v1.11: previously described ambiguously as “the OI geometry”; now explicitly the combined/submitted polygon.)Frontend · Flink
GET/drones/{serial}/oi/current/flight-volume(v1.11) Fetch the Flight Volume geometry alone — the trajectory expanded by the FV buffers, as its own GeoJSON polygon.Frontend (Map) · Compliance AI
GET/drones/{serial}/oi/current/contingency-volume(v1.11) Fetch the Contingency Volume geometry alone — the trajectory expanded by the CV buffers, as its own GeoJSON polygon. 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 }. Cheap — no polygons in the payload; geometry_ref links to the three geometry endpoints above. The same block appears inline in GET /drones/{serial}.Frontend · MCP · Compliance AI
WS/drones/{serial}/current-oi/watchSubscribe to current-OI state transitions (SUBMITTED → ACCEPTED → ACTIVE → COMPLETED) and regeneration events. Frontend uses this to update the OI badge without polling.Frontend · OCC
GET/drones/{serial}/oi/historyList previous OI generations for this drone within a time range, including all three stored geometries per generation. For audit and FLYplan post-flight review.Compliance AI · Evidence

OI Configuration — New Required Fields

Three new OI geometry parameters have been added to the drone configuration. These are stored in drones.drones and exposed through the OI Automation API endpoints.

FieldDefaultValid rangeDescription
oi_automation_square_m 700 m 350 – 1400 m OI square side length used in the FLYplan geometry calculation.
oi_automation_radius_m 400 m 200 – 800 m OI radius used in the FLYplan geometry calculation.
oi_automation_amend_min_trigger_m 100 m 50 – 200 m Minimum aircraft displacement (m) required to trigger an OI amendment.
Proportionality constraint. These three values were tuned together and must scale together. The valid range for each field is exactly 0.5× to 2.0× of its default. The frontend should present them as a single factor slider (0.5 – 2.0) rather than three independent inputs — e.g. factor 1.5 → 1050 m / 600 m / 150 m. The server enforces individual field ranges but does not enforce proportionality; the UI is responsible for keeping all three in sync.
API contract changes for OI geometry fields.

B. ADS-B Provisioning

Per-drone ADS-B configuration. v1 uses a single surrogate boolean; the schema notes anticipate splitting into in/out/runtime fields in a future revision (see DM-DI in §10 Open Questions).

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. Body: { in_enabled, out_enabled, surrogate_enabled }.Frontend
PUT/drones/{serial}/adsb-surrogateToggle just the ADS-B Surrogate flag. Convenience endpoint; equivalent to a PATCH on the block.Frontend · MCP
PUT/drones/{serial}/tisb-rebroadcastToggle the TIS-B Rebroadcast flag (tisb_rebroadcast_enabled). Body: { enabled: bool }. When enabled, the Custodian rebroadcasts this aircraft’s own telemetry into the TIS-B feed so ground-based ADS-B receivers can observe it.Frontend · MCP
GET/drones/{serial}/adsb/statusRead the live ADS-B status from telemetry: is the transponder actually broadcasting? Last observed ICAO hex? Last seen by CIFIB?Frontend · Compliance AI

C. Ownership Flag

The Ownership flag is the operator’s explicit surveillance intent for one of their drones. When set, surveillance services (Flight_Live, §9) tag inbound records of this aircraft as own-aircraft so consumers can apply their per-purpose filtering.

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 }. The reason field supports audit.Frontend · MCP (permission-scoped)

D. Custodian Configuration (adapter, defaults, boot)

The configuration block the Custodian itself fetches at boot. Carries adapter type, GCS plugin endpoints, broker addresses, publish rate, and OI defaults bundled as one document.

EndpointPurposePrimary Caller
GET/drones/{serial}/configCustodian boot config — the full configuration block the Custodian needs to instantiate. Returns adapter type, publish rate, ADS-B block, OI defaults, ownership flag, etc.Custodian (on boot)
GET/drones/{serial}/config?since={ts}Returns config only if changed since the given timestamp. Supports cheap polling from the Custodian for change detection.Custodian (watch loop)
WS/drones/{serial}/config/watchWebSocket subscription that pushes config changes as they happen. Optional — polling is the simpler fallback.Custodian (optional)
PUT/drones/{serial}/adapter-configUpdate adapter-specific connection block (e.g. MAVLink host/port, DJI FH2 endpoint).Frontend
PUT/drones/{serial}/publish-rateChange the telemetry publish rate (Hz). Custodian hot-reloads.Frontend · MCP

E. State Read & Telemetry Ingestion

State-read endpoints for consumers, and the ingestion endpoint the GCS Plugins POST 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. The canonical dTwin view.All consumers
GET/drones/{serial}/stateJust the live state subset (position, mode, battery, link, adapter_health) without configuration noise. Cheaper for high-frequency consumers.Map (live layer)
GET/drones/{serial}/current-flightFetch the current-flight block: { mission_id, state, window, waypoints, progress_pct }. Reflects the mission the drone is executing right now (v1.9). The same block appears inline in GET /drones/{serial}.Frontend · MCP · Compliance AI
WS/drones/{serial}/current-flight/watchSubscribe to flight-state transitions (PLANNED → ACTIVE → COMPLETED) and waypoint progress updates. Frontend uses this to drive the mission-progress bar.Frontend
GET/drones/{serial}/telemetryRecent telemetry timeseries (positions, modes, battery) within a time range. Backed by InfluxDB.Map (trails) · Evidence
WS/drones/{serial}/state/watchSubscribe to live state updates for one drone. Replaces the v1.5 MQTT topic subscription for direct UI use cases.Frontend (map live layer)
POST/drones/{serial}/telemetryIngestion endpoint. The GCS Plugin POSTs normalized telemetry frames here. The Custodian receives, normalizes again if needed, writes the live-state columns of tenant.drones, and appends to InfluxDB.GCS Plugins (Mission Planner, Chrome)
POST/drones/{serial}/alertsPlugin-emitted alerts (mode change, RTL triggered, geofence breach, low battery). Routed to OCC and stored in evidence.GCS Plugins · OCC AI

F. Mission Binding

Binds an active mission to a drone for the duration of an OI window. This is how callsign ↔ serial association becomes authoritative (used by Flink’s ADS-B network_id resolution).

EndpointPurposePrimary Caller
GET/drones/{serial}/missionRead the active mission binding (mission_id, callsign, OI window, pilot_id).Frontend · Flink · Compliance AI
POST/drones/{serial}/missionCreate a mission binding. Body includes mission_id, callsign, OI window start/end, pilot_id.FLYsafe.software (on mission publish)
DELETE/drones/{serial}/missionClear the active mission binding (after flight completion or cancellation).FLYsafe.software · Frontend

G. Identity & Cross-Tenant Routing

Identity registration in Keycloak and cross-tenant publication into AADMS. Run by the Import process and by the Custodian on first boot.

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 for cross-tenant routing of inbound ADS-B records.Import process · Custodian
GET/drones/{serial}/identityInspect identity state — Keycloak client ID, AADMS ID, registration timestamps.Frontend (admin view)

H. Health & Lifecycle

Health probes for the Custodian, and lifecycle events (drone retired, archived, restored).

EndpointPurposePrimary Caller
GET/drones/{serial}/healthHealth of the Custodian for this drone: adapter_connected, last_telemetry_at, plugin_session_id, plugin_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 (re-enable telemetry acceptance).Frontend (admin)

I. Pilot & Separation Defaults (v1.7)

Per-drone default pilot binding and conflict-evaluation minima. These become the defaults every mission and OI inherit unless the operator overrides at mission publication.

EndpointPurposePrimary Caller
GET/drones/{serial}/default-pilotRead the default pilot for this drone: { name, fs_guid }. Used as the pre-filled pilot when a new mission is created without explicit selection.Frontend · MCP
PUT/drones/{serial}/default-pilotSet the default pilot. Body: { name: string, fs_guid: string }. The fs_guid is validated against FLYsafe.software’s pilot registry before the write is committed.Frontend
DELETE/drones/{serial}/default-pilotClear the default pilot binding for this drone.Frontend
GET/drones/{serial}/separation-defaultsRead the per-drone NMAC, SWC, and EVAL distances in metres, split by crewed/uncrewed traffic and horizontal/vertical (v1.12): { nmac_crewed_m: {h,v}, nmac_uncrewed_m: {h,v}, swc_crewed_m: {h,v}, swc_uncrewed_m: {h,v}, eval_distance_m }.Frontend · MCP · Flink (OI Automation)
PUT/drones/{serial}/separation-defaultsSet NMAC, SWC, and EVAL distances. Body accepts any subset of the eight NMAC/SWC fields plus eval_distance_m. Server-side validation enforces nmac < swc < eval for each matching horizontal/vertical and crewed/uncrewed pair, and clamps to sane bounds.Frontend
What NMAC / SWC / EVAL mean in FLYsafe. Near Mid-Air Collision (NMAC) is the tightest allowed proximity before a compliance event fires. Stay Well Clear (SWC) is the buffer distance a mission must maintain from other traffic. Evaluation (EVAL) is the outer ring at which the OI Automation and DAA services start reasoning about a potential encounter. Each drone can override the tenant-wide defaults with values appropriate to its platform. (v1.12) NMAC and SWC are each split into four values — horizontal and vertical minima, separately for crewed (manned) and uncrewed (other drone) traffic — because the tolerable proximity to a helicopter is not the same as to another small drone, and horizontal and vertical separation are rarely equal. EVAL remains a single scalar.

J. Data Source Monitoring (v1.7)

Read-only surface for the vigilance state the Custodian maintains for each drone. See §3.6 for the concept; these endpoints expose it to consumers.

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 }. Rate expectations are derived from publish_rate_hz already on the drone.Frontend (admin)
WS/drones/{serial}/data-source/watchWebSocket that pushes status transitions (HEALTHY → DEGRADED, DEGRADED → STALE, etc.) as they happen. Optional; polling GET /drones/{serial}/data-source is the fallback.OCC AI (live)
GET/drones/{serial}/data-source/historyTime-range query of status transitions and rate/latency samples for post-flight review. Backed by InfluxDB. Feeds CAR 922 evidence — connectivity gaps are part of the operational record.Compliance AI · Evidence

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, and health without restriction; writes are scoped to operationally-safe toggles only, and require explicit per-tenant agent permissions.

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. (v1.7)
drone.set_default_pilotPUT /drones/{serial}/default-pilotWPermission-scoped; validates against FLYsafe.software. (v1.7)
drone.get_separation_defaultsGET /drones/{serial}/separation-defaultsRNMAC / SWC (crewed/uncrewed × horizontal/vertical) / EVAL in metres. (v1.7; split v1.12)
drone.set_separation_defaultsPUT /drones/{serial}/separation-defaultsWServer-side clamp enforces nmac < swc < eval per h/v & crewed/uncrewed pair. (v1.7; split v1.12)
drone.get_data_sourceGET /drones/{serial}/data-sourceRSource-vigilance summary. High value for OCC AI. (v1.7)
drone.get_data_source_historyGET /drones/{serial}/data-source/historyRPost-flight connectivity review; feeds Compliance AI. (v1.7)
drone.get_volumesGET /drones/{serial}/volumesRFV and CV buffer defaults. (v1.8)
drone.set_volumesPUT /drones/{serial}/volumesWServer-side clamp enforces CV ≥ FV in both dimensions. (v1.8)
drone.get_current_flightGET /drones/{serial}/current-flightRCurrent mission state; high value for OCC AI and Compliance AI. (v1.9)
drone.get_current_oiGET /drones/{serial}/current-oiRCurrent OI summary (state, window, regeneration times, geometry_ref). (v1.9)
drone.get_submitted_volumeGET /drones/{serial}/oi/currentRThe polygon actually sent to DSS (FV + CV combined). Heavy payload. (v1.11)
drone.get_flight_volume_geometryGET /drones/{serial}/oi/current/flight-volumeRFV polygon alone. Heavy payload; used by Compliance AI. (v1.11)
drone.get_contingency_volume_geometryGET /drones/{serial}/oi/current/contingency-volumeRCV polygon alone. Heavy payload; used by Compliance AI. (v1.11)
drone.get_waypoint_bufferGET /drones/{serial}/waypoint-bufferRRadius in metres for waypoint arrival detection. (v1.10)
drone.set_waypoint_bufferPUT /drones/{serial}/waypoint-bufferWServer-side clamp: 5 m ≤ value ≤ 500 m. (v1.10)

Operations not on this list (retire, identity registration, mission binding writes, adapter-config changes, retire/restore) are deliberately not exposed via MCP. They can be added incrementally as the agent permission model matures.

Authentication. All API calls carry a JWT issued by the tenant’s Keycloak realm. The JWT identifies the caller (operator, GCS plugin session, MCP agent) and is the basis for both authorization (which endpoints they can hit) and audit (who made the change). GCS plugins use the operator’s session token; MCP agents use per-agent service accounts with scoped permissions.

6. Identity Model — Four-Tier Layout

FLYsafe.live carries a drone’s identity across four systems. Each tier has a distinct purpose; mixing them up breaks the binding pipeline.

System Identity Key Holds Purpose
FLYsafe.software DLB dlb_guid Authoritative drone inventory — brand, model, serial, TC registration, core RID fields System of record for inventory
tenant.drones + dlb_drones → v_dtwin (rtm-app-db) (tenant_id, serial_number) drones.drones: RTM-managed config + live state + dlb_guid FK. drones.dlb_drones: verbatim DLB identity. v_dtwin: the JOIN that forms the complete dTwin record. The dTwin lives here (v_dtwin view)
Keycloak.θ.Drone <tenant>-<serial> Drone-as-user authentication — credentials, group membership, service access Authentication only
AADMS (tenant, serial) + callsign Cross-tenant identity lookup — callsign → tenant routing for inbound ADS-B records that don’t carry a tenant Cross-tenant routing
Drone-as-user vs drone-as-asset. Stephen Smith’s framing in the Jun 15 Daily Technical Review: “What do we need to know about it to give it access to the appropriate resources? That goes in Keycloak. What do we know about that drone as an asset? That goes in Postgres.” Keycloak carries credentials and access; the Postgres row carries everything else.

The four identifiers a drone carries

Serial Number — primary binding key Callsign — ADS-B identifier (per-mission) ICAO 24-bit Hex — dynamic session address TC Registration — regulatory display

Serial number is the only one used as a binding key across the system. Callsign is set per-mission and used only for ADS-B inbound resolution via AADMS. ICAO hex is dynamic per session (uAvionix-aligned ADDRt) and is never bound to. TC registration is regulatory display.

7. FLYplan Workflow — What the OI Automation APIs Drive

The FLYplan process is the automated creation and maintenance of Operational Intents (OIs) for live flights. The Custodian APIs (§5A) are the surface that operators and AI agents use to turn this on, set its parameters, and observe its output.

StepWhat happensAPI call(s)
1. Onboard the drone Operator sets per-drone OI defaults (distance, speed limits), volume defaults (FV buffer, CV buffer — horizontal only), separation defaults (NMAC / SWC, each crewed/uncrewed × horizontal/vertical, plus EVAL), and default pilot (name + FLYsafe.software GUID). These become the baseline every mission and auto-generated OI inherits. PUT /drones/{serial}/oi-defaults
PUT /drones/{serial}/volumes
PUT /drones/{serial}/separation-defaults
PUT /drones/{serial}/default-pilot
2. Pilot grants OI Automation authority Pilot flips the per-drone OI Automation toggle on the Inventory page of the RTM Map (or an AI agent does it on the pilot’s behalf with scoped permission). This is the explicit act that authorizes the Custodian and Flink to manage the drone’s OI lifecycle autonomously going forward (v1.10). Flink’s OI Automation job begins watching this drone’s telemetry. PUT /drones/{serial}/oi-automation
3. Mission published FLYsafe.software publishes a mission with (serial, callsign, time window, waypoints). The Custodian binds the mission and populates current_flight_state = PLANNED along with the mission window and waypoint total. POST /drones/{serial}/mission
4. Drone airborne, telemetry flowing GCS Plugin (or Cloud Broker) posts telemetry to RTTP, which the Custodian consumes to update position, flight_mode, armed in tenant.drones. It also flips current_flight_state = ACTIVE on first armed-airborne detection. The flight-monitoring job advances current_waypoint_index and current_progress_pct when the aircraft enters the waypoint_buffer_m radius (default 50 m) around the next expected waypoint — sequential matching, so a missed waypoint holds the index. POST /drones/{serial}/telemetry
reads GET /drones/{serial}/waypoint-buffer
5. OI regenerated Flink’s OI Automation job predicts the trajectory and reads the drone’s FV and CV buffers, then computes three geometries: the trajectory expanded by FV buffers becomes the Flight Volume; expanded further by CV buffers becomes the Contingency Volume; the two are then combined into a single Submitted Volume polygon. All three are written to flights_binding (v1.13). Only the Submitted Volume is transmitted to FLYrtm DSS. On acceptance, current_oi_id / current_oi_state are updated (ACCEPTED then ACTIVE). Regeneration is triggered whenever the predicted trajectory would breach either volume. Internal Flink job
reads GET /drones/{serial}/oi-defaults
reads GET /drones/{serial}/volumes
writes flights_binding
updates current_oi block
6. Operator or agent reviews RTM Map renders the current OI geometry and reads the current-flight / current-oi summary blocks for the mission strip. Operator can adjust defaults mid-flight. (v1.13) RTM Map does not surface a manual “regenerate OI” action — regeneration is a backend-only process driven by Flink; MCP agents/ops tooling can still request a forced regeneration. OCC AI monitors OI conformance via the watch streams. GET /drones/{serial}/oi/current
GET /drones/{serial}/current-oi
GET /drones/{serial}/current-flight
POST /drones/{serial}/oi/regenerate
7. Mission completes Custodian flips current_flight_state = COMPLETED and current_oi_state = COMPLETED. Mission binding cleared; both blocks retained until the next mission binds. OI history retained for post-flight review and compliance evidence. DELETE /drones/{serial}/mission · GET /drones/{serial}/oi/history
This is the highest-value workflow. FLYplan touches almost every Custodian API category — defaults, automation toggling, mission binding, telemetry ingestion, state read, and post-flight review. Getting these APIs right remains the top sprint priority.

8. Surveillance & Ownership Tagging

Flight_Live is the surveillance microservice that observes live_ads, live_community, and live_vision streams. In v1.6 it consults each drone’s ownership_flag_enabled (read from the Custodian API) to tag incoming records as own-aircraft or external.

📌 Decision Record (Jun 15, 2026) — Preserve all surveillance data

v1.4 extracted “ghost” records at ingest. v1.5+ tags records instead and lets them flow through; each downstream consumer (Map, DAA, Blender, Analytics) applies its own filtering policy.

“Don’t throw away good data... let it flow through and filter it out at the very last minute before you when you don’t need it. Later on we’ll turn on the fusing and we will do fusing at multiple different locations. But if we don’t even have the data, we can’t fuse it.”

— Lindsay Mohr, Jun 15 Daily Technical Review

“We want to be able to go later on after the event, go and compare what was the MAVLink location versus what was the ADSB location and do lookups and telemetry. So why would we filter that information out, throw it away, not send it through?”

— Lindsay Mohr, same review

How Flight_Live reads ownership state. Flight_Live queries the Custodian API (GET /drones/{serial}/ownership) to maintain an in-memory ownership index per tenant. The index is rebuilt on startup from the API; live changes flow in via either polling or the optional WebSocket watch endpoint. No MQTT subscription is required.

Per-stream tagging algorithms and the full Flight_Live spec live in the 859ze-4217 wiki subpage. The reframed v1.6 contract is: Flight_Live emits a unified surveillance_traffic stream where every record carries ownership_flagged: bool and owner_serial if matched. Downstream consumers decide what to do with the flag.

9. Lifecycle & Operational Model

Lifecycle EventWhat Happens
Drone added in DLB Next Import sync inserts the drone into tenant.drones with default RTM config. Identity created in Keycloak (<tenant>-<serial>). Published to AADMS.
Operator installs plugin Mission Planner Plugin (or Chrome GCS Plugin) loaded, authenticates against the tenant’s Keycloak, ready to forward telemetry for any drone in scope.
Custodian boots Server-side Custodian instance loads; calls GET /drones/{serial}/config for its drone; begins accepting telemetry posts from the plugin.
Telemetry frame arrives Plugin POSTs to /drones/{serial}/telemetry. Custodian normalizes, writes live-state columns to tenant.drones, appends to InfluxDB. last_telemetry_at updated.
Pilot grants/toggles OI Automation authority Frontend calls PUT /drones/{serial}/oi-automation. Row updated. This is the pilot-authority act (v1.10) — once granted, the Custodian owns current_flight_state/current_oi_state transitions autonomously until revoked. Custodian and Flink job pick up the change on their next poll.
Source rate degrades Rate drops below the expected envelope or p95 latency exceeds threshold. Custodian sets data_source_status = DEGRADED. Map shows warning halo. OCC AI is notified via the data-source/watch stream.
Plugin disconnects No telemetry for N × the expected interval. Custodian sets data_source_status = STALE then DISCONNECTED once an explicit disconnect is observed, and adapter_connected = false. Frontend and OCC see the state change.
Drone armed & airborne Custodian detects armed + non-zero groundspeed from telemetry, flips current_flight_state = ACTIVE. Waypoint index advances as position matches successive waypoints within waypoint_buffer_m.
OI accepted by DSS Flink OI Automation submits OI to FLYrtm DSS; on acceptance, Custodian writes current_oi_id / current_oi_state = ACCEPTED then ACTIVE.
Mission completes Custodian flips current_flight_state = COMPLETED and current_oi_state = COMPLETED. Mission binding cleared via DELETE /drones/{serial}/mission. Both blocks retained until the next mission binds. OI history retained.
Drone retired POST /drones/{serial}/retire. Row marked retired; live state cleared; Keycloak user disabled.

10. Open Questions & Next Steps

Tracked across the wiki subpages under TECH-002/FSL RTTP.

Recently Resolved

QuestionStatusResolution
Current-flight state machine authority. Who owns transitions on current_flight_state? RESOLVED Pilot-granted authority model (v1.10). The pilot grants OI Automation authority to the Custodian by flipping the per-drone toggle on the RTM Map Inventory page. Once granted, the Custodian owns state transitions autonomously (armed+airborne → ACTIVE, mission complete → COMPLETED, etc.). Pilot can revoke by flipping the toggle off. See callout in §5A.
Waypoint matching heuristic. How close to a waypoint is “reached”? RESOLVED Per-drone waypoint_buffer_m, default 50 m (v1.10). Sequential matching — a missed waypoint holds current_waypoint_index. Buffer settable per-drone on the RTM Map Inventory page. Respected by the Flink flight-monitoring jobs.
WebSocket watch endpoints vs polling. Ship WS endpoints, or rely on polling for the first cut? RESOLVED Shipped as optional companions to polling: /config/watch, /state/watch, /current-flight/watch, /current-oi/watch, /data-source/watch. Polling remains a valid fallback for every one of them.
Frontend integration note. The pilot-authority toggle and the waypoint-buffer numeric input are per-drone controls on the RTM Map Inventory page, alongside the existing Custodian configuration controls. Frontend owner: JC.

📝 Changelog — v1.13 (Aug 20, 2026)

What changed from v1.12

📝 Changelog — v1.12 (Aug 20, 2026)

What changed from v1.11

Resolved against the RTM Map mockup during the developer gap-review (see FSL_Database_Structure.html §4) — the mockup's data model wins on both points below; the schema and API docs are updated to match it.

📝 Changelog — v1.11 (Jun 22, 2026)

What changed from v1.10

📝 Changelog — v1.10 (Jun 20, 2026)

📌 v1.10 changes (preserved)

📝 Changelog — v1.6 (Jun 16, 2026)

📌 v1.6 changes (preserved)

📐 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 (v1.13) 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.

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-06-16T18:42:00Z" },
    { "lat": 53.5480, "lon": -113.4901, "alt_msl_m": 725.1, "t": "2026-06-16T18:43:30Z" }
    // ...
  ],
  "fv_buffer_m": 56.0,                          // horizontal only (v1.12)
  "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-06-16T18:42:18.600Z"
}

Functional requirements

RequirementDetail
FV polygon Horizontal only (v1.12): 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 polygon Same 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 Volume The 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.
Determinism Same trajectory + same buffers must always produce the same three polygons, byte-for-byte. Regeneration diffing (§7 step 5) depends on this.
Latency Called on every OI regeneration tick (§7 step 5), so this must return well within Flink’s regeneration cadence. Target: p95 < 200 ms per call.
No side effects Purely 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.