tenant.drones Postgres, exposing it through APIs that the frontend uses today and MCP agents will use tomorrowOne 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.
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.
tenant.drones, kept current by the Custodian. Queryable, joinable, durable — the basis for all downstream consumers.tenant.drones current. Other systems read state from there.(tenant, serial). Callsign, ICAO hex, TC registration are properties, not keys.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.
rtm-app-db.drones. It writes only DLB-sourced columns; RTM-managed columns are left for the Custodian and operators to manage.(tenant, serial). Carries both DLB-sourced fields (read-only via sync) and RTM-managed fields (writable via Custodian APIs).tenant.drones — that is its primary job. Telemetry enters via the GCS Plugins (§③), gets normalized, and the Custodian writes state through the API.<tenant>-<SN>. This is auth-only — no drone metadata lives here.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.
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.
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.
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.
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:
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).
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).
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 |
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.
The Custodian calls the APIs to obtain its own configuration at boot — GET /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.
For: ArduPilot, PX4, Sentaeros 6
Configuration: autopilot host/port, MAVLink dialect, heartbeat interval, reconnect delay, system ID, component ID filter, message rate overrides
Served via the Mission Planner Plugin’s configuration endpoint
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
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
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
{ "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.
tenant.dronesThe 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.
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. |
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.
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);
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);
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;
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 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.
drones.dlb_drones and drones.drones work togetherdrones.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.
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.
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 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.
dlb_drones into drones.dronesdlb_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. |
|
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.
| Category | Primary store | Writer | Examples |
|---|---|---|---|
| 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 |
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 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.
This is the catalog of API operations the Aircraft Custodian exposes. The same APIs are consumed by:
tenant.drones.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.
drones.drones. This gives per-consumer field isolation and stable contracts as the schema evolves.
Operations supporting the FLYplan workflow — automated OI creation, per-drone OI defaults, OI automation toggling. The most active surface for the v1.6 sprint.
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/oi-automation | Read the OI Automation status for a drone (enabled, defaults, last regeneration time). | Frontend · MCP |
| PUT/drones/{serial}/oi-automation | Enable 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-defaults | Set 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-defaults | Read the current OI defaults. | Frontend · MCP · Custodian (boot) |
| POST/drones/{serial}/oi/regenerate | Force-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/current | Fetch the currently active OI geometry for this drone (GeoJSON polygon, with active mission reference). | Frontend · Flink |
| GET/drones/{serial}/oi/history | List previous OI generations for this drone within a time range. For audit and FLYplan post-flight review. | Compliance AI · Evidence |
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.
| Field | Default | Valid range | Description |
|---|---|---|---|
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. |
defaults block now includes all three fields.enabled: true — returns 422 if any of the three fields is null; ensure they are set before enabling OI automation.ConfigSaveBody accepts the three new fields; same null-before-enable constraint applies when oi_automation_enabled: true is sent.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).
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/adsb | Read ADS-B configuration block: in_enabled, out_enabled, surrogate_enabled. | Frontend · MCP · Custodian |
| PUT/drones/{serial}/adsb | Update the full ADS-B configuration block in one call. Body: { in_enabled, out_enabled, surrogate_enabled }. | Frontend |
| PUT/drones/{serial}/adsb-surrogate | Toggle just the ADS-B Surrogate flag. Convenience endpoint; equivalent to a PATCH on the block. | Frontend · MCP |
| PUT/drones/{serial}/tisb-rebroadcast | Toggle 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/status | Read the live ADS-B status from telemetry: is the transponder actually broadcasting? Last observed ICAO hex? Last seen by CIFIB? | Frontend · Compliance AI |
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.
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/ownership | Read the current ownership flag state. | Frontend · Flight_Live |
| PUT/drones/{serial}/ownership | Set the ownership flag. Body: { enabled: bool, reason?: string }. The reason field supports audit. | Frontend · MCP (permission-scoped) |
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.
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/config | Custodian 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/watch | WebSocket subscription that pushes config changes as they happen. Optional — polling is the simpler fallback. | Custodian (optional) |
| PUT/drones/{serial}/adapter-config | Update adapter-specific connection block (e.g. MAVLink host/port, DJI FH2 endpoint). | Frontend |
| PUT/drones/{serial}/publish-rate | Change the telemetry publish rate (Hz). Custodian hot-reloads. | Frontend · MCP |
State-read endpoints for consumers, and the ingestion endpoint the GCS Plugins POST to.
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones | List 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}/state | Just the live state subset (position, mode, battery, link, adapter_health) without configuration noise. Cheaper for high-frequency consumers. | Map (live layer) |
| GET/drones/{serial}/telemetry | Recent telemetry timeseries (positions, modes, battery) within a time range. Backed by InfluxDB. | Map (trails) · Evidence |
| WS/drones/{serial}/state/watch | Subscribe 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}/telemetry | Ingestion 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}/alerts | Plugin-emitted alerts (mode change, RTL triggered, geofence breach, low battery). Routed to OCC and stored in evidence. | GCS Plugins · OCC AI |
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).
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/mission | Read the active mission binding (mission_id, callsign, OI window, pilot_id). | Frontend · Flink · Compliance AI |
| POST/drones/{serial}/mission | Create a mission binding. Body includes mission_id, callsign, OI window start/end, pilot_id. | FLYsafe.software (on mission publish) |
| DELETE/drones/{serial}/mission | Clear the active mission binding (after flight completion or cancellation). | FLYsafe.software · Frontend |
Identity registration in Keycloak and cross-tenant publication into AADMS. Run by the Import process and by the Custodian on first boot.
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| POST/drones/{serial}/identity/keycloak | Register the drone as a user in Keycloak. ID format <tenant>-<serial>. Idempotent. | Import process · Custodian (boot) |
| POST/drones/{serial}/identity/aadms | Publish the drone’s (tenant, serial, callsign) tuple to AADMS for cross-tenant routing of inbound ADS-B records. | Import process · Custodian |
| GET/drones/{serial}/identity | Inspect identity state — Keycloak client ID, AADMS ID, registration timestamps. | Frontend (admin view) |
Health probes for the Custodian, and lifecycle events (drone retired, archived, restored).
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/health | Health of the Custodian for this drone: adapter_connected, last_telemetry_at, plugin_session_id, plugin_version. | OCC · Monitoring · Compliance AI |
| POST/drones/{serial}/retire | Mark drone as retired. Stops accepting telemetry, clears live state, archives the row. | Frontend (admin) |
| POST/drones/{serial}/restore | Restore a retired drone (re-enable telemetry acceptance). | Frontend (admin) |
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 name | Maps to | Read/Write | Notes |
|---|---|---|---|
drone.list | GET /drones | R | List drones in scope. |
drone.get | GET /drones/{serial} | R | Full record. |
drone.get_state | GET /drones/{serial}/state | R | Live state only. |
drone.get_telemetry | GET /drones/{serial}/telemetry | R | Time-range query. |
drone.set_oi_automation | PUT /drones/{serial}/oi-automation | W | Operationally safe. |
drone.set_oi_distance | PUT /drones/{serial}/oi-defaults | W | Numeric clamp applied. |
drone.set_adsb_surrogate | PUT /drones/{serial}/adsb-surrogate | W | Operationally safe. |
drone.set_ownership | PUT /drones/{serial}/ownership | W | Permission-scoped. |
drone.regenerate_oi | POST /drones/{serial}/oi/regenerate | W | Operationally safe. |
drone.get_health | GET /drones/{serial}/health | R | Used 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.
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 |
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.
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.
| Step | What happens | API 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 |
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.
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
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 Event | What 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. |
Tracked across the wiki subpages under TECH-002/FSL RTTP. Highlights for v1.6:
/drones/{serial}/telemetry? (Maykon + Tim)app_user to per-identity DB roles for human-driven writes; trace each updated_at back to its JWT.tenant.drones Postgres. APIs are the contract for everything else (frontend reads/writes, Custodian writes from telemetry, MCP agent tools).tenant.drones and managed through the Custodian APIs. The Custodian itself fetches its own configuration at boot via these same APIs.<strong>, <em>, <code>) inside SVG <text> elements with valid <tspan> equivalents — restoring rendering of the Solution Concept and System Architecture diagrams.