🚁 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.6 · API-Driven State & GCS Plugin Architecture

🎯 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 ① AIRCRAFT & GCS ② GCS PLUGINS & BROKERS ③ CUSTODIAN SW ④ RTTP ⑤ CUSTODIAN APIs ⑥ 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 ⑦ 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 (v1.6)

① 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 (§③) · Normalizes · Maintains dTwin state Writes to tenant.drones via Custodian APIs (§⑤) { 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

② 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

Honest telemetry flow with explicit failure declaration. While its GCS Plugin is healthy and forwarding data, the Custodian updates state at the configured cadence. When the plugin disconnects, the Custodian sets adapter_health.connected = false.

Observable via: adapter_health.connected, last_publish_ago_ms, the API’s health endpoint.

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

③ 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

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

④ 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,
  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_m                   float8 DEFAULT 56.0 NOT NULL,
  contingency_vol_m                 float8 DEFAULT 150.0 NOT NULL,

  -- 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,

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

  -- Ownership flag (v1.5: replaces ghost extraction; see §⑧)
  ownership_flag_enabled            bool DEFAULT false NOT 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
  keycloak_client_id                text NULL,               -- drone-as-user reference
  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.flight_volume_m,
  d.contingency_vol_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,

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

  -- 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.keycloak_client_id,
  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.

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

① 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.
② 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.
③ 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 ②). 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 ③). 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, adsb_surrogate_enabled, tisb_rebroadcast_enabled, ownership_flag_enabled
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 §⑨
  },
  "oi": {
    "automation_enabled": true,
    "default_distance_m": 300
  },
  "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.

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.

⑤ 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. The most active surface for the v1.6 sprint.

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 }. When enabled, the Flink OI Automation job will regenerate the drone’s OI continuously based on its trajectory.Frontend · MCP
PUT/drones/{serial}/oi-defaultsSet OI default parameters: horizontal_speed_ms, vertical_speed_ms, flight_volume_m, contingency_vol_m, 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)
POST/drones/{serial}/oi/regenerateForce-regenerate the OI now (skip waiting for the next trajectory tick). Useful for testing or after a defaults change.Frontend · MCP
GET/drones/{serial}/oi/currentFetch the currently active OI geometry for this drone (GeoJSON polygon, with active mission reference).Frontend · Flink
GET/drones/{serial}/oi/historyList previous OI generations for this drone within a time range. 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 §⑩ 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, §⑨) 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}/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)

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.

Operations not on this list (retire, identity registration, mission binding writes, adapter-config changes, retire/restore) are deliberately not exposed via MCP in v1.6. 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.

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

⑦ 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 (§⑤A) 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, flight volume, contingency volume. These become the baseline for every auto-generated OI. PUT /drones/{serial}/oi-defaults
2. Enable OI Automation Operator (or AI agent) toggles OI Automation on for a drone. 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). The Custodian binds the mission to the drone. POST /drones/{serial}/mission
4. Drone airborne, telemetry flowing GCS Plugin posts telemetry to the Custodian. Custodian updates position, flight_mode, armed in tenant.drones. POST /drones/{serial}/telemetry
5. OI regenerated Flink’s OI Automation job predicts trajectory breaches against the current OI, regenerates the OI geometry, submits to FLYrtm DSS. Internal Flink job · reads via GET /drones/{serial}/oi-defaults
6. Operator or agent reviews RTM Map renders the current OI. Operator can adjust defaults mid-flight; agents can request a forced regeneration; OCC AI can monitor OI conformance. GET /drones/{serial}/oi/current, POST /drones/{serial}/oi/regenerate
7. Mission completes Mission binding cleared; 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 for v1.6. FLYplan touches almost every Custodian API category — defaults, automation toggling, mission binding, telemetry ingestion, state read, and post-flight review. Getting these APIs right is the v1.6 sprint priority.

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

⑨ 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.
Operator toggles OI Automation Frontend calls PUT /drones/{serial}/oi-automation. Row updated. Custodian and Flink job pick up the change on their next poll.
Plugin disconnects No telemetry for N seconds. Custodian sets adapter_connected = false. Frontend and OCC see the state change.
Mission completes Mission binding cleared via DELETE /drones/{serial}/mission. OI history retained.
Drone retired POST /drones/{serial}/retire. Row marked retired; live state cleared; Keycloak user disabled.

⑩ Open Questions & Next Steps

Tracked across the wiki subpages under TECH-002/FSL RTTP. Highlights for v1.6:

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

What changed from v1.5