FLYsafe.live — MQTT Topic Reference

Visual reference for the MQTT broker architecture, topic namespace design, and per-category topic inventory. Read alongside the ClickUp wiki page FLYsafe_MQTT_Topic_Strategy.md.

v1.2 — June 2026 Middleware · Flink · Web App teams Draft for developer review
1. Broker architecture

Three broker types. Each has a distinct role and a distinct set of consumers.

Shared platform broker
One — for all tenants
emqx-mqtt.stage.iart.software

Publishes surveillance feeds and weather. Topics are pushed to every tenant broker on arrival. No aircraft telemetry lives here.

Tenant broker
One per organisation
emqx-mqtt.{tenant}.stage.flysafe.live

All aircraft telemetry, external ingest, internal coordination, and the shared feeds re-published from the platform broker. The web app subscribes here only.

Kafka (internal)
Microservice bus
internal — not exposed

Raw ingest, Flink pipeline topics, commands, dead-letter queues. Never consumed by the web app or the MQTT broker directly.

Data flow — shared feeds to tenant brokers
Shared broker
surveillance + weather
airmarket broker
v1/airmarket/shared/…
Web app
one broker subscription
Same shared topics also pushed to sait broker, and any future tenant broker
💡
One broker per tenant. The web app subscribes to the tenant broker only and receives everything — including the shared surveillance and weather feeds — in one connection. No multi-broker subscription complexity in the client.
2. Topic path anatomy

Every topic on the tenant broker follows this pattern. Each segment has a specific purpose.

v1 / airmarket / aircraft / SIMTL70 / drones / telemetry
v1
Schema version. Breaking schema changes ship as v2/… in parallel. Consumers migrate on their own schedule. Never increment without a notice period.
airmarket
Tenant ID. Stays in path even with one broker per tenant — visible in cross-tenant federated subscriptions without parsing the payload. Use wildcard v1/+/aircraft/… to subscribe across tenants.
aircraft
Scope type. Determines the ACL boundary and topic structure. One of four values: aircraft (own per-drone telemetry), shared (tenant-wide feeds), internal (microservice coordination — never web app), or partner (cross-tenant partnership streams). Partner topics omit the {tenant} segment entirely — the broker is already scoped to one tenant, so it is implicit in the connection. The literal keyword partner occupies segment 2, providing immediate visual disambiguation between own-drone and partner feeds without redundant path segments, followed by {partnerTenant} to identify the data source: v1/partner/sait/aircraft/SAI-003/drones/telemetry.
SIMTL70
Serial / ID. Drone serial number. Wildcard + here subscribes to all drones: v1/airmarket/aircraft/+/drones/telemetry.
drones
App context. Maps 1-to-1 with the app navigation context. Segment 5 always tells you which UI context a message belongs to: drones · oi · surveillance · infrastructure · rtm-services · inventory · internal
telemetry
Message type. Specific message type within the context: telemetry · networkid · c2links · events · tisb · declaration · osd
Variable — changes per topic
Fixed keyword — always literal
Context-aligned — matches app nav
3. Topic categories — three types on every tenant broker

Every topic is one of three categories. The category determines who produces it, who consumes it, and whether the web app ever sees it.

RTTP telemetry
Web app facing
Published by FlightBinder (Flink) after normalisation. Consumed directly by the web app via WSS. All field names and units are canonical — no autopilot-specific knowledge in the client.
v1/{tenant}/aircraft/{sn}/drones/telemetry
External ingest
Adapter facing
Third-party data arriving in native formats. Flink adapter jobs subscribe, transform to canonical schema, and republish to RTTP telemetry topics. The web app never sees raw external payloads.
thing/product/{sn}/osd
Internal system
Microservice facing
Coordination signals between backend services. Dock-drone pairing, asset positions, C2 station tracking. Must never be consumed by the web app — not part of the public topic contract.
v1/{tenant}/internal/dock-status/{sn}
Partnership streams
Partner-facing
Cross-tenant drone telemetry arriving via KafkaConnector from an approved partner tenant. Already in canonical RTTP schema — no transform needed at consume time. Web app subscribes read-only. Never published by own-tenant Flink jobs.
v1/partner/{B}/aircraft/{sn}/drones/telemetry
⚠️
Internal topics must never reach the web app. Configure EMQX ACL rules to prevent any client with a web app credential from subscribing to v1/{tenant}/internal/#.
4. Shared platform broker — topic inventory

These topics originate on the shared broker and are re-published to each tenant broker under v1/{tenant}/shared/…

Surveillance traffic — air targets
Topic (on shared broker)Re-published as (on tenant broker)RateDescription
surveillance_traffic/live_mlatv1/{t}/shared/surveillance_traffic/live_mlat1–5 HzMLAT-fused aircraft tracks. 5 Jetvision G-1090 stations — 2 clusters: CYEG area + Fort Saskatchewan, 37.2 km baseline.
surveillance_traffic/live_communitysurveillance_traffic/live_community1–5 HzCommunity ADS-B — 989 global receivers. dump1090/readsb JSON format. Dedup by ICAO hex across feeds.
surveillance_traffic/live_regional_{region}surveillance_traffic/live_regional_{region}1–5 HzRegional feeds: EDM (23 receivers), CAL (55 receivers), FMM (provisioned, empty on staging).
surveillance_traffic/live_adssurveillance_traffic/live_ads1–5 HzAggregated ADS-B feed. Slightly different schema — no r/t fields, extended emergency string.
surveillance_traffic/live_visionsurveillance_traffic/live_visionOn detectGACM vision sensor aircraft detections. Includes emitter_device_id. Subset of ADS-B fields.
Surveillance sensors — station health
TopicRateDescription
surveillance_sensors/live_mlat30–60 s5/5 online. G-1090 + G-1090 UAT hardware. Note: no per-device connected or uptime fields — station health inferred from envelope online_receivers.
surveillance_sensors/live_vision30–60 s2/2 online. GACM-0100-000086 + 000114. Includes altitude_mm, rtt_ms, uptime per device. altitude_mm ÷ 1000 = metres.
surveillance_sensors/live_community30–60 s989/989 global receivers. Unique schema: has distance_miles_edm, distance_miles_fmm, distance_miles_cal per receiver — pre-computed from all three Alberta cities.
surveillance_sensors/live_regional_{region}30–60 sEDM (23 receivers), CAL (55 receivers). Single distance_miles field from regional centre.
Weather
TopicRateDescription
weather/VariesAll weather subtopics. Full subtopic structure TBD — see Open Item OI-02. Feeds Drones — Weather tab.
5. Tenant broker — RTTP telemetry topics

Published by FlightBinder (Flink) after normalisation. All values are canonical — correct units, decoded field names, no raw integers or sentinel values.

Per-aircraft — path prefix: v1/{tenant}/aircraft/{serial}/
TopicRateApp contextKey fields
drones/telemetry10 Hz Drones lat, lng, heading, agl/aglM, asl/aslM, atl/atlM, flightMode, armed, battery, groundSpeed, vertSpeed, hAcc, vAcc, utmState
drones/networkid~1 Hz Drones heading (degrees direct), alt_agl (m direct), pilot (full_name), callsign, missionName, missionLocation, waypoints[], trackOrigin
drones/c2links1 Hz Drones — C2 tab per-link: label, tech, status, latency, rssi, rfScore, isLead, uplink, downlink; mobile station: lat, lng, vehicleCallsign
drones/eventsOn event Drones — Events timestamp, category (Comms/Mission/DAA/Power/OI/System), severity (ok/warn/crit), message
drones/tisbOn change Drones — Surv tab cifib_status (connected/disconnected), last_update
oi/declarationOn change OI's oiPolygon[] (Leaflet format), conformance, status, maxAgl, start/end, subscribers[], utmProvider, declarationId
Shared — path prefix: v1/{tenant}/shared/
TopicRateApp contextKey fields
surveillance_traffic/live_ads
surveillance_traffic/live_mlat
surveillance_traffic/live_community
surveillance_traffic/live_vision
surveillance_traffic/live_regional_edmonton
surveillance_traffic/live_regional_calgary
surveillance_traffic/live_regional_fort_mcmurray
1–5 Hz Surveillance hex, callsign (trimmed), lat, lon, alt_baro (ft), gs (kt), track, baro_rate (ft/min), isMlat (bool), isTisb, trust, rssi (dBm), seenSec
surveillance_sensors/live_mlat
surveillance_sensors/live_vision
surveillance_sensors/live_community
surveillance_sensors/live_regional_edmonton
surveillance_sensors/live_regional_calgary
surveillance_sensors/live_regional_fort_mcmurray
30–60 s Surveillance online_receivers, expected_receivers, devices[].id, devices[].lat, devices[].lon, devices[].rtt_ms, devices[].uptime
weather/Varies Drones — Wx conditions, tempC, windSpeedKt, windDir, gustKt, ceilingFt, metar, forecast[]
infrastructure/sites30 s Infrastructure site.id, site.status, site.lat/lng, backhaul, latency, uptime, component.status, component.model
rtm-services/platform5 s RTM Services service.name, service.status, kafkaLag, flinkJobs[], cpuPct, memPct, errorRate, processingRate
inventory/assetsOn change Inventory droneId, model, serial, uasType, deploymentType, ec.adsbOut, ec.adsbIn, dockId, assignedPilot
💡
Wildcard subscriptions. Web app subscribes to all own drones: v1/airmarket/aircraft/+/drones/telemetry. All partner drones (any active partner, all subtopics): v1/partner/+/aircraft/+/#. All own-tenant contexts: v1/airmarket/#. Note: v1/airmarket/# does not capture partner topics by design — partner data lives under the separate v1/partner/ root, ensuring clean ACL separation between own-fleet and cross-tenant feeds.
Partnership streams — path prefix: v1/partner/{partnerTenant}/aircraft/{serial}/
ℹ️
Why no {tenant} in the partner path? Each EMQX broker instance is dedicated to a single tenant — the tenant is already implicit in the broker connection endpoint. Including {tenant} in the partner path would be redundant and forces the frontend to parse the second segment just to determine whether a message is own-fleet or partner data.

The design instead uses the literal keyword partner as segment 2, in place of {tenant}. This gives the frontend an immediate, unambiguous signal at the topic level: a message on v1/tenant/… belongs to the operator's own fleet; a message on v1/partner/… comes from an approved cross-tenant feed. The keywords themselves carry the semantic distinction — no payload parsing or topic matching logic is needed to route messages to the correct map layer.

A tenant can have more than one active partner simultaneously. The {partnerTenant} segment identifies the source of each feed. A single wildcard subscription v1/partner/+/aircraft/+/# covers all active partners — new partnerships appear and disappear automatically without any frontend reconfiguration when the Partnership Controller creates or deletes a KafkaConnector.
TopicRateApp contextKey fields
drones/telemetry10 Hz Partner Drones Identical schema to own-drone drones/telemetry. Carries an additional partner_tenant field injected by the KafkaConnector transform. Web app renders with distinct visual treatment — grey icon, "Partner: {B}" label. Provides live position, heading, altitude (AGL/ASL), flight mode, armed state, battery, and ground speed. Wildcard for all active partners: v1/partner/+/aircraft/+/drones/telemetry.
drones/networkid~1 Hz Partner Drones Identical schema to own-drone drones/networkid. Provides the receiving tenant with operator context for the partner drone: pilot full name, callsign, mission name, mission location, and active waypoints. Enables the frontend to identify which partner operator and mission plan are associated with a drone currently on the map.
oi/declarationOn change Partner OIs Identical schema to own-drone oi/declaration. Streams the partner drone's active Operational Intent: OI polygon (Leaflet format), conformance status (CONFORMING / NON_CONFORMING), max AGL, declared start and end times, subscribers, UTM provider, and declaration ID. Allows the receiving tenant to visualise the partner's OI boundary alongside the drone position and monitor conformance in real time.
ℹ️
Source & lifecycle. All three subtopics are produced by a partner-{B}-to-{A}-mqtt-sink KafkaConnector in the shared-kafka namespace, reading from {B}.telemetry.enriched. The connector routes per-serial — the Kafka message key (aircraft serial) builds the MQTT topic path. Partnership must be approved via the Partnership Controller API before messages flow: creating the connector starts streaming; deleting it stops it immediately. See Cross-Tenant Partnership Architecture for the full request → approve → suspend → resume lifecycle.

ACL. Web app clients may subscribe to v1/partner/+/aircraft/+/# — publish is denied. The v1/partner/# namespace is never produced by the tenant's own Flink jobs; it is exclusively written by KafkaConnectors in shared-kafka, enforcing a clear boundary between own-fleet output and inbound partner streams.
6. Tenant broker — external ingest topics

Third-party data arriving in native formats. Flink adapter jobs normalise and republish. The web app never subscribes to these topics.

⚠️
External ingest topics live at the broker root — no namespace prefix. DJI and weather cannot be routed to nested topics due to constraints of the external systems. They publish directly to root-level topics. Flink adapter jobs subscribe and route to the correct Kafka topics.
DJI Cloud API — root level
Topic (root)RateKafka outputNotes
thing/product/{sn}/osd1–5 Hz{tenant}.telemetry.raw.djiOSD = On-Screen Display. host key = dock/GCS. sub key = aircraft (airborne only).
thing/product/{sn}/#Various{tenant}.telemetry.raw.djiAll DJI message types via one wildcard subscription.
DJI Bridge adapter — required conversion rules
data.sn
Dock serial — look up aircraft serial via dronesense_dock_status before attributing OSD data to a drone
host key
Dock / GCS hardware telemetry — always present. Maps to dock.* and c2.* canonical fields.
sub key
Aircraft telemetry — only present when drone is airborne. Maps to position, altitude, battery, flight mode fields.
quality == 65535
DJI sentinel = "not measured". Map to null. Never pass 65535 to the web app — it is not a valid signal quality value.
departure_point 0,0
Home point not yet acquired. Map lat/lon 0,0 to null in rth.homePt.
tilt_angle.value
Radians → degrees: multiply by 57.2958. Sample: 0.5978 rad = 34.2°
link_workmode
1 = SDR primary link. 0 = 4G primary link. Map to c2.workmode string.
oiPolygon coordinates
DJI / GeoJSON convention is [longitude, latitude]. Leaflet expects [latitude, longitude]. Swap all coordinate pairs before publishing.
7. Tenant broker — internal system topics

Microservice coordination signals. Current topics use flat unversioned names. Migration to v1/{tenant}/internal/… namespace is recommended.

Internal topics — current + proposed names
Current topicProposed topicConsumerPurpose
dronesense_dock_status/{dock_sn} v1/{t}/internal/dock-status/{sn} DJI Bridge adapter Dock↔drone serial pairing registry. Required lookup before DJI OSD can be attributed to the correct aircraft. dock_sn ≠ drone_sn.
asset_tracking/ops_asset/{asset_id} v1/{t}/internal/asset-tracking/ops/{id} Asset tracker service + infra map Operational asset positions (trucks, docks, vehicles). Dock variant (subcategory=Dock) includes on-site weather: temperature, humidity, wind_speed, rainfall — feeds wx.groundSensors[].
asset_tracking/c2_monitoring/{station_id} v1/{t}/internal/asset-tracking/c2/{id} C2 Monitor service Mobile C2 station position. GRS station is vehicle-mounted on chase truck — horizontal_speed > 0 when moving. Feeds C2 tab station geolocation. Must update dynamically.
⚠️
Parallel publish during migration. Rename internal topics by publishing to both old and new paths simultaneously for a transition period. Internal consumers can cut over to the new path independently.
Key findings from payload analysis
Dock = weather station
The asset_tracking/ops_asset/{dock_id} payload with subcategory=Dock carries info.temperature, info.humidity, info.wind_speed, info.environment_temperature, info.rainfall. This can populate the Drones weather tab ground sensors for sites with a dock — no separate weather API needed.
Drone in dock state
info.drone_in_dock == 1 confirms drone is physically docked. info.drone_charge_state.capacity_percent gives battery while docked. info.sub_device.device_online_status gives aircraft agent connectivity.
RTK quality from dock
info.position_state.rtk_number (29 sats), is_fixed == 2 (RTK fixed), quality == 5 (best). This is the dock's GPS quality, not the aircraft's.
8. Kafka internal topics

Used by Flink jobs and microservices only. Never exposed to the MQTT broker or the web app.

Kafka topic naming — {tenant}.{domain}.{subdomain}

Kafka uses dots as separators (not slashes). The tenant prefix scopes all topics to the organisation. Partition key is aircraft_serial within each topic — no per-serial topic splitting.

Telemetry & networkid
Kafka TopicPurposeFlink consumer
{tenant}.telemetry.rawRaw MAVLink from RTTP agents (ArduPilot / PX4)FlightBinder
{tenant}.telemetry.raw.djiRaw DJI Cloud API telemetry — separate ingest path from RTTPDJI Bridge adapter
{tenant}.telemetry.enrichedFlightBinder canonical output — MQTT Bridge publishes this to the web appMQTT Bridge
{tenant}.networkidRaw networkid messages from aircraft agentNetworkID processor
{tenant}.networkid.ddDeduplicated / processed networkid — FlightBinder reads .dd not .rawFlightBinder
OI / UTM / Flight plan
Kafka TopicPurposeFlink consumer
{tenant}.flightplanFlight plan / OI declaration dataOI Conformance Tracker
{tenant}.utmUTM / DSS state messages — OI conformance changesOI Conformance Tracker
Events — one topic per event type
ℹ️
Events are not a single catch-all topic. Each Flink CEP job publishes to its own typed event topic. The Alert Router subscribes to all {tenant}.events.* topics.
Kafka TopicPurposeAlert Router output
{tenant}.events.c2C2 link state change eventsuser alerts if primary/all links lost
{tenant}.events.containmentContainment / geofence breach eventsWARNING → ops team
{tenant}.events.daaDAA conflict / breach eventsCRITICAL → ops team
{tenant}.events.operatorOperator-facing alert events→ user/{id}/alerts directly
{tenant}.events.rtlRTL (Return to Launch) triggered eventsWARNING → ops team
{tenant}.events.utmUTM state change events (OI conformance)WARNING if non-conforming
Internal system & infrastructure
Kafka TopicPurposeConsumer
{tenant}.dronesenseDroneSense dock status / pairing dataDJI Bridge adapter
{tenant}.asset.tracking.ops.asset.dockAsset tracking — dock position + on-site weather telemetryAsset tracker service
{tenant}.mqtt_bridgeMQTT bridge relay outputMQTT Bridge
{tenant}.mqtt_sink_dlqDead-letter queue for MQTT sink failuresOps / retry service
Real topic names — airmarket tenant
airmarket.telemetry.raw
airmarket.telemetry.raw.dji
airmarket.telemetry.enriched
airmarket.networkid
airmarket.networkid.dd
airmarket.flightplan
airmarket.utm
airmarket.events.c2
airmarket.events.containment
airmarket.events.daa
airmarket.events.operator
airmarket.events.rtl
airmarket.events.utm
airmarket.dronesense
airmarket.asset.tracking.ops.asset.dock
airmarket.mqtt_bridge
airmarket.mqtt_sink_dlq
ℹ️
Partition key: {serial} for per-aircraft topics — all messages for one drone processed in order by the same Flink instance. {tenant_id} for shared topics. Retention: raw ingest 7 d · enriched 24 h · events 30 d · DLQ 14 d.
9. Proximate aircraft topic — per-drone

A dedicated per-drone topic streams aircraft within the DAA evaluation rings to both the web app and the aircraft agent. Critical path for DAA services — each consumer gets the same message, no topic duplication.

Data flow — DAA proximity job
Fused surveillance targets
shared/surveillance/fused
Drone telemetry
aircraft/{sn}/drones/telemetry
DAA ring config
eval / SWC / NMAC radii
DAA Proximity Job
Flink — keyed by drone serial
computes distance + closure rate
assigns ring: EVAL / SWC / NMAC
drones/proximate_aircraft
aircraft/{serial}/drones/proximate_aircraft
Web app
renders NMAC/SWC/Eval rings
Aircraft agent
onboard DAA logic
Wildcard sub all drones:
aircraft/+/drones/proximate_aircraft
Payload fields
FieldTypeDescription
tsint msEvaluation timestamp (Unix ms)
droneIdstringDrone serial — self-describing alongside topic path
evalRadius / swcRadius / nmacRadiusfloat mRing radii in metres — tenant/drone-configurable, not hardcoded
evalCount / swcCount / nmacCountintTarget count per ring — for UI badge display
targets[].hexstringICAO hex — primary target ID
targets[].callsignstringTrimmed ATC callsign
targets[].lat / .lngfloat degTarget position
targets[].alt_baro / .alt_geomfloat ftBarometric and geometric altitude
targets[].gs / .track / .baro_ratekt / deg / ft/minSpeed, heading, vertical rate
targets[].distM / .distNmfloatDistance from drone — primary DAA field
targets[].bearingfloat degBearing from drone to target (true north)
targets[].closureRatefloat m/sClosure rate — positive = approaching
targets[].ringenumEVAL / SWC / NMAC
targets[].isMlat / .isTisb / .trust / .feedSourcemixedSource provenance and trust level
10. Events vs alerts — two separate queues

Events and alerts serve different audiences and travel different pipelines. A single event may trigger zero, one, or many alerts depending on configurable routing rules.

Events
System → system
Operational facts that happened. Consumed by the web app Events tab and Flink CEP jobs. Every state change is recorded. Retained 30 days — queryable. Not all events need a user alert.
aircraft/{sn}/drones/events
Alerts
System → human
Notifications requiring human attention. Scoped to the aircraft — whoever is monitoring that drone subscribes to its alert topic. No per-user routing needed. The web app subscribes to alerts for whichever drones are active on screen.
aircraft/{sn}/alerts · shared/alerts/broadcast
Events → Alert Router → aircraft-scoped alert topics
Flink CEP jobs
detect state changes
drones/events
what happened · stored 30 d
→ web app Events tab
→ further Flink CEP
Alert Router (Flink)
event → should alert? who?
aircraft/{sn}/alerts
aircraft/{sn}/alerts/{severity}
shared/alerts/broadcast
Notification svc
Event → alert routing rules
Event Alert? Severity Notify
DAA
utmState == EMERGENCYYESCRITICALAll users
DAA NMAC breachYESCRITICALOps team
DAA SWC breachYESWARNINGOps team
Target enters eval ringnoEvent only — no alert
C2 links
All C2 links lostYESCRITICALOps + pilot
Primary link lostYESWARNINGOps team
Link restorednoEvent only
Power
Battery < 15%YESWARNINGOps team
Battery < 5%YESCRITICALAll users
Battery at 30%noEvent only — logged
OI / Mission
OI non-conformingYESWARNINGOps team
Mission completeYESINFOOps team
Flight mode changenoEvent only — logged
Alert routing rules are configurable — stored in Alert Router config, not hardcoded · Threshold values (15%, 5%, ring radii) must be tenant-configurable
Alert topics — aircraft-scoped
✈️
Alerts are scoped to the aircraft, not the user. Whoever is monitoring a drone — operator, pilot, observer — subscribes to that aircraft's alert topic. The web app subscribes to alerts for whichever drones are active on screen. No per-user routing, no user identity required.
TopicPurposeWho subscribes
v1/{tenant}/aircraft/{serial}/alertsAll alerts for this aircraft — DAA, C2, battery, OI, missionWeb app (per monitored drone) · Aircraft agent (own serial only)
v1/{tenant}/aircraft/{serial}/alerts/{severity}Filtered by severity: critical / warning / infoSelective subscribers — automated response systems, AI agent
v1/{tenant}/shared/alerts/broadcastTenant-wide system alerts not tied to a single aircraft (platform outage, service degradation)Web app — always subscribed regardless of which drone is in focus
Web app wildcard — all active drones: v1/airmarket/aircraft/+/alerts — single subscription covers all aircraft. Client filters by serial to route each alert to the correct drone panel.
Payload schemas — events & alerts
Event payload · drones/events
FieldTypeDescription
tsint msUnix timestamp (ms) when event occurred
droneIdstringAircraft serial number
tenantIdstringTenant identifier
categoryenumComms · Mission · DAA · Power · OI · System
severityenumok · warn · crit
eventTypestringMachine-readable identifier (snake_case)
messagestringHuman-readable event description
dataobjectCategory-specific detail fields — see examples below
Alert payload · aircraft/{sn}/alerts
FieldTypeDescription
tsint msUnix timestamp (ms) when alert was issued
alertIdstringUnique alert identifier — prefixed ID or UUID
droneIdstring|nullAircraft serial — null for broadcast alerts
tenantIdstringTenant identifier
severityenumcritical · warning · info
categoryenumDAA · C2 · Power · OI · System
titlestringShort display title for UI notification
messagestringFull alert message for display
sourceEventstringeventType that triggered this alert
requiresAckboolWhether user acknowledgement is required
ackedAtint ms|nullAcknowledgement timestamp — null if pending
ackedBystring|nullUser ID who acknowledged — null if pending
Test & development example payloads

Ready-to-publish payloads for each event and alert category. Use these in MQTT Explorer, a test harness, or the EMQX dashboard to simulate flight conditions during development.

ℹ️
Test drone: SIMTL70 · Tenant: airmarket · Topic prefix: v1/airmarket/aircraft/SIMTL70/ — replace ts values with the current Unix epoch in milliseconds when publishing.
Event examples — topic: v1/airmarket/aircraft/SIMTL70/drones/events
Comms — primary link lost (warn)
{
  "ts": 1718467200000,
  "droneId": "SIMTL70",
  "tenantId": "airmarket",
  "category": "Comms",
  "severity": "warn",
  "eventType": "link_primary_lost",
  "message": "Primary C2 link lost — SDR offline",
  "data": {
    "linkLabel": "SDR",
    "tech": "SDR",
    "previousStatus": "active",
    "currentStatus": "offline",
    "backupActive": true,
    "backupTech": "4G"
  }
}
DAA — NMAC breach (crit)
{
  "ts": 1718467320000,
  "droneId": "SIMTL70",
  "tenantId": "airmarket",
  "category": "DAA",
  "severity": "crit",
  "eventType": "daa_nmac_breach",
  "message": "NMAC breach — C172 within 152m",
  "data": {
    "targetHex": "C00F12",
    "targetCallsign": "CNC172",
    "distM": 152.4,
    "closureRate": 14.7,
    "ring": "NMAC",
    "targetAltFt": 2340,
    "droneAglM": 91.5
  }
}
Power — battery < 15% (warn)
{
  "ts": 1718467440000,
  "droneId": "SIMTL70",
  "tenantId": "airmarket",
  "category": "Power",
  "severity": "warn",
  "eventType": "battery_low",
  "message": "Battery at 13% — return recommended",
  "data": {
    "batteryPct": 13,
    "threshold": 15,
    "estimatedFlightTimeSec": 210,
    "voltageV": 21.6,
    "currentA": 18.3
  }
}
OI — non-conforming (warn)
{
  "ts": 1718467560000,
  "droneId": "SIMTL70",
  "tenantId": "airmarket",
  "category": "OI",
  "severity": "warn",
  "eventType": "oi_non_conforming",
  "message": "Aircraft outside OI boundary",
  "data": {
    "declarationId": "oi-2024-06-15-001",
    "conformance": "NON_CONFORMING",
    "deviationM": 47.2,
    "maxAglM": 120,
    "currentAglM": 134.5,
    "missionName": "Pipeline Survey North"
  }
}
Mission — complete (ok)
{
  "ts": 1718468100000,
  "droneId": "SIMTL70",
  "tenantId": "airmarket",
  "category": "Mission",
  "severity": "ok",
  "eventType": "mission_complete",
  "message": "Mission completed successfully",
  "data": {
    "missionName": "Pipeline Survey North",
    "missionId": "msn-2024-06-15-003",
    "durationSec": 1740,
    "waypointsTotal": 12,
    "waypointsCompleted": 12,
    "pilotName": "Jordan Ellis"
  }
}
System — flight mode change (ok)
{
  "ts": 1718467680000,
  "droneId": "SIMTL70",
  "tenantId": "airmarket",
  "category": "System",
  "severity": "ok",
  "eventType": "flight_mode_change",
  "message": "Flight mode changed to AUTO",
  "data": {
    "previousMode": "LOITER",
    "currentMode": "AUTO",
    "armed": true,
    "flightMode": "AUTO"
  }
}
Alert examples — topic: v1/airmarket/aircraft/SIMTL70/alerts
DAA — NMAC breach (critical)
{
  "ts": 1718467320000,
  "alertId": "alrt_daa_7f3a9c",
  "droneId": "SIMTL70",
  "tenantId": "airmarket",
  "severity": "critical",
  "category": "DAA",
  "title": "NMAC Breach",
  "message": "C172 (C00F12) within 152m — immediate action required",
  "sourceEvent": "daa_nmac_breach",
  "requiresAck": true,
  "ackedAt": null,
  "ackedBy": null
}
C2 — all links lost (critical)
{
  "ts": 1718467200000,
  "alertId": "alrt_c2_2b8f1e",
  "droneId": "SIMTL70",
  "tenantId": "airmarket",
  "severity": "critical",
  "category": "C2",
  "title": "All C2 Links Lost",
  "message": "All C2 links offline — aircraft in failsafe",
  "sourceEvent": "link_all_lost",
  "requiresAck": true,
  "ackedAt": null,
  "ackedBy": null
}
Power — battery < 15% (warning)
{
  "ts": 1718467440000,
  "alertId": "alrt_pwr_9d4c5a",
  "droneId": "SIMTL70",
  "tenantId": "airmarket",
  "severity": "warning",
  "category": "Power",
  "title": "Low Battery",
  "message": "Battery at 13% — return to home recommended",
  "sourceEvent": "battery_low",
  "requiresAck": false,
  "ackedAt": null,
  "ackedBy": null
}
Broadcast — topic: v1/airmarket/shared/alerts/broadcast
{
  "ts": 1718467800000,
  "alertId": "alrt_sys_3e2a1f",
  "droneId": null,
  "tenantId": "airmarket",
  "severity": "warning",
  "category": "System",
  "title": "Platform Degraded",
  "message": "FlightBinder latency elevated — telemetry may be delayed",
  "sourceEvent": "flink_job_lag_high",
  "requiresAck": false,
  "ackedAt": null,
  "ackedBy": null
}
💡
Testing workflow. Publish event payloads to v1/airmarket/aircraft/SIMTL70/drones/events to verify the Events tab renders. Publish alert payloads to v1/airmarket/aircraft/SIMTL70/alerts to verify the alert panel. The Alert Router normally produces alerts automatically from events — in manual testing, publish both independently. Use MQTT Explorer or: mosquitto_pub -h emqx-mqtt.airmarket.stage.flysafe.live -t "v1/airmarket/aircraft/SIMTL70/alerts" -m '{...}'
11. Migration strategy — legacy to strategic MQTT structure

The current FLYsafe RTM app subscribes to a set of legacy flat topics with no tenant prefix. The strategic design introduces a structured namespace (v1/{tenant}/…), a separate cross-tenant partnership stream (v1/partner/…), and several net-new topic types. This section defines the full porting path for frontend developers and the parallel-publish strategy that makes the migration zero-downtime.

Three structural changes in the strategic design
Change 1 — Tenant namespace
All own-drone topics gain a structured prefix: v1/{tenant}/aircraft/{serial}/…. The tenant ID is explicit in the path — it removes ambiguity in ACL rules and future-proofs cross-tenant federation without changing the per-tenant broker model.
Change 2 — Context hierarchy
Flat topic names become hierarchical app-context paths. networkid/{serial} becomes v1/{tenant}/aircraft/{serial}/drones/networkid. The context segment (drones/, oi/) maps 1-to-1 with the RTM UI navigation context — no per-serial subscription needed; one wildcard covers all drones.
Change 3 — Partnership streams
A new v1/partner/{partnerTenant}/aircraft/{serial}/… root carries approved cross-tenant drone feeds. It is a separate namespace from v1/{tenant}/… — purely additive, no legacy migration required. One frontend wildcard (v1/partner/+/aircraft/+/#) subscribes to all active partners at once.
Current — legacy topics (v1 app)
FLYsafe RTM v1
Flat topic names, no tenant prefix, no partnership support. Remains active during the parallel publish window. The web app subscribes directly to these.
networkid/{serial}
flightplan/{serial}
surveillance_traffic/{feed}
TISB Stream/{serial}
thing/product/{sn}/osd ← middleware-internal
— no partnership support —
Strategic — new structure (v2 app)
FLYsafe RTM v2
Namespaced own-drone topics, shared feeds, and a separate partnership root. Developed and tested in parallel against the new paths during the transition window.
v1/{tenant}/aircraft/{sn}/drones/networkid
v1/{tenant}/aircraft/{sn}/oi/declaration
v1/{tenant}/shared/surveillance_traffic/{feed}
v1/{tenant}/aircraft/{sn}/drones/tisb
v1/{tenant}/aircraft/{sn}/drones/telemetry
v1/partner/{partnerTenant}/aircraft/{sn}/… ← new
Topic-by-topic migration map
Legacy topic (v1 app)Strategic topic (v2 app)PriorityNotes
Own-drone telemetry — v1/{tenant}/aircraft/{serial}/…
networkid/{serial} …/drones/networkid P1 Primary normalised source — heading, AGL, pilot name, callsign, mission name, waypoints. Most critical topic to port. Wildcard: v1/{tenant}/aircraft/+/drones/networkid.
flightplan/{serial} …/oi/declaration P1 OI polygon (Leaflet format), conformance state (CONFORMING / NON_CONFORMING), max AGL, start/end times, declarationId. Renamed from flightplan to reflect the operational intent concept. Wildcard: v1/{tenant}/aircraft/+/oi/declaration.
TISB Stream/{serial} …/drones/tisb P2 TIS-B CIFIB connection status. On-change only. Normalised topic name; payload schema unchanged.
Shared feeds — v1/{tenant}/shared/…
surveillance_traffic/{feed} …/shared/surveillance_traffic/{feed} P2 Payload and feed names (live_ads, live_mlat, live_community, live_regional_*) are unchanged. Subscription path update only — add the v1/{tenant}/shared/ prefix. Middleware Surveillance Fusion Job publishes to both old and new paths during the transition window.
External ingest — unchanged at broker root (middleware-internal, not subscribed by web app)
thing/product/{sn}/osd v1/{tenant}/aircraft/{sn}/drones/telemetry
(produced by DJI Bridge adapter)
P3 DJI publishes to the root-level thing/product/ path — this does not change. The DJI Bridge adapter normalises OSD payloads into the canonical drones/telemetry schema. The web app never subscribes to thing/product/… directly — it always consumed normalised output.
Partnership streams — v1/partner/{partnerTenant}/aircraft/{serial}/… — net new, no legacy equivalent
— no legacy topic — v1/partner/{B}/aircraft/{sn}/drones/telemetry NEW Partner drone live position, heading, altitude, flight mode, battery. Produced by a KafkaConnector in shared-kafka on partnership approval. Wildcard for all active partners: v1/partner/+/aircraft/+/drones/telemetry.
— no legacy topic — v1/partner/{B}/aircraft/{sn}/drones/networkid NEW Partner drone mission context — pilot name, callsign, mission name, waypoints. Allows the receiving tenant to identify who is flying and under what mission plan for each partner drone on the map.
— no legacy topic — v1/partner/{B}/aircraft/{sn}/oi/declaration NEW Partner drone's active Operational Intent — OI polygon, conformance status, max AGL, start/end times, declarationId. Renders the partner's OI boundary alongside their drone position and monitors conformance in real time.
Net-new topics — no legacy equivalent, v2 app only
TopicContextWhat it enables
v1/{t}/aircraft/{sn}/drones/telemetryDronesCanonical 10 Hz drone position direct from FlightBinder — lat, lng, heading, AGL, ASL, flight mode, armed state, battery, ground speed. Primary own-fleet map feed.
v1/{t}/aircraft/{sn}/drones/c2linksDrones — C2 tabPer-link C2 telemetry — SDR/4G label, latency, RSSI, RF score, primary/backup designation, mobile station lat/lng. Powers the C2 tab and chase-truck map pin.
v1/{t}/aircraft/{sn}/drones/eventsDrones — EventsTyped event stream (Comms / Mission / DAA / Power / OI / System). Powers the Events tab; feeds the Alert Router for downstream alerting.
v1/{t}/aircraft/{sn}/drones/proximate_aircraftDrones — DAADAA proximity rings — all air traffic within EVAL / SWC / NMAC radii, with distance, bearing, and closure rate per target. Powers the DAA map overlay.
v1/{t}/aircraft/{sn}/alertsAlertsPer-drone alert topic. Frontend subscribes to all active drones via wildcard: v1/{t}/aircraft/+/alerts. Routes by serial to the correct drone panel.
v1/{t}/shared/alerts/broadcastAlertsPlatform-wide broadcast alerts not tied to one aircraft — service degradation, planned maintenance. Always subscribed regardless of which drone is in focus.
v1/partner/+/aircraft/+/#PartnerAll cross-tenant partnership streams. A single wildcard covers all active partners — drone positions, mission context, and OI boundaries start flowing the moment a partnership is approved, with no frontend reconfiguration.
🤝
Partnership is additive — nothing migrates, nothing breaks. Partner topics live under v1/partner/…, a completely separate root from v1/{tenant}/…. There is no legacy topic to cut over from. To enable partnership support the frontend adds three wildcard subscriptions: v1/partner/+/aircraft/+/drones/telemetry, …/drones/networkid, and …/oi/declaration. No existing subscription changes. Partner data begins flowing the moment the Partnership Controller approves a request and creates the KafkaConnector — the wildcard picks it up automatically.
Frontend subscription checklist — v1 → v2 porting guide
Replace all subscriptions in the Remove block before the v1 parallel-publish window closes. Add everything in the Add blocks from day one of v2 development — they are live and publishable now. Replace {tenant} with your tenant ID (e.g. airmarket).
↳ Remove — unsubscribe from legacy topics when v2 is in production
networkid/{serial}
Replace with v1/{tenant}/aircraft/+/drones/networkid — single wildcard covers all drones, no per-serial subscription needed
flightplan/{serial}
Replace with v1/{tenant}/aircraft/+/oi/declaration — renamed to reflect the operational intent concept; schema extended with conformance state
surveillance_traffic/{feed}
Replace with v1/{tenant}/shared/surveillance_traffic/{feed} — payload and feed names unchanged; prefix only
TISB Stream/{serial}
Replace with v1/{tenant}/aircraft/+/drones/tisb — normalised name; same on-change behaviour
↳ Add — own-drone subscriptions (new topics, no legacy equivalent)
v1/{t}/aircraft/+/drones/telemetry
10 Hz canonical position from FlightBinder — primary map feed for own-fleet drones. Replaces DJI raw OSD (which was always middleware-internal)
v1/{t}/aircraft/+/drones/c2links
Per-link C2 detail — feeds the C2 tab; provides mobile station lat/lng for the chase-truck map pin
v1/{t}/aircraft/+/drones/events
Typed event stream — feeds Events tab; the Alert Router derives alert topics from these events
v1/{t}/aircraft/+/drones/proximate_aircraft
DAA proximity rings — renders EVAL / SWC / NMAC rings and target list on the map for each active drone
v1/{t}/aircraft/+/alerts
Per-drone alert topic — one wildcard covers all aircraft; route to the correct drone panel by droneId field
v1/{t}/shared/alerts/broadcast
Platform-wide alerts — subscribe once at startup, always active regardless of which drone is in focus
↳ Add — partnership subscriptions (net-new, no migration required)
v1/partner/+/aircraft/+/drones/telemetry
All active partner drone positions — one wildcard, all partners simultaneously. Render as grey icon, "Partner: {partnerTenant}" label. Extract partner identity from partner_tenant payload field
v1/partner/+/aircraft/+/drones/networkid
Partner mission context — pilot name, callsign, mission name. Populate the partner drone detail panel on click
v1/partner/+/aircraft/+/oi/declaration
Partner OI polygon and conformance — render the boundary alongside the partner drone, styled distinctly from own-tenant OIs
Parallel publish strategy — zero downtime migration
1
Middleware publishes to BOTH namespaces simultaneously. Each topic is published to the legacy path AND the new v1/{tenant}/… path. No existing consumer breaks. Partnership topics under v1/partner/… begin publishing immediately when the first KafkaConnector is created — they have no legacy path to maintain.
2
v2 app is built and tested against the new namespace. Frontend subscribes to v1/{tenant}/… own-drone topics and adds v1/partner/… partnership subscriptions. Both app versions run in parallel — v1 on legacy paths, v2 on the strategic structure — without interfering with each other.
3
v2 reaches feature parity — switch traffic over. When v2 passes acceptance testing, route production users to v2. v1 remains on standby as rollback for an agreed window. Partnership features are live in v2 from the first approved partnership — no additional release required.
4
Decommission legacy topic paths. After an agreed notice period, middleware stops publishing to legacy paths and v1 is retired. v1/partner/… topics are a live production capability — they are never decommissioned.
12. Open items

Unresolved items and confirmed resolutions.

IDItem / ResolutionStatusOwner
OI-01surveillance_traffic/ structure NOT changing. Parent topic with sub-topics per service. live_ads has reduced field set vs live_community — missing r, t, mlat[], rssi. Fusion Job handles both schemas.✅ ResolvedMiddleware
OI-02Topic is weather/. Subtopic structure and payload TBD — to be provided by weather service team.⏳ PartialWeather team
OI-03dronesense_dock_status produced by FLYsafe microservices AFTER OSD messages arrive. DJI Bridge must queue OSD until pairing available — do not drop.✅ ResolvedDroneSense
OI-04Asset IDs are human-labelled in microservice config. Stable per deployment.✅ ResolvedAsset tracker
OI-05GRS = Ground Radio Station. Stable per vehicle per deployment.✅ ResolvedC2 team
OI-06Internal topic migration to v1/{tenant}/internal/ namespace. Parallel publish approach confirmed. Timeline TBD.⏳ In progressMiddleware
OI-07DLQ monitoring — developer monitors during development. AI agent monitoring under consideration for production.⏳ In progressMiddleware / Ops
OI-08Partnership connector source topic: prefer {partnerTenant}.telemetry.enriched over .raw — enriched is already canonical RTTP schema, web app receives correct units and decoded fields with no additional transform. Confirm Tenant B's enriched topic is published into the shared-kafka namespace before the connector is built.⏳ OpenTenant B middleware