{
  "openapi": "3.1.0",
  "info": {
    "title": "Farsight Feeds API",
    "description": "HTTP API for Farsight Feeds catalog, dataset, feed, upstream, and GeoJSON layer access.\n\nSame surface is also available via gRPC on port 50051 (see\n`proto/feed_adapter.proto`). Use gRPC for Rust→Rust IPC; HTTP for\neverything else.\n",
    "version": "1.0.0",
    "license": {
      "name": "AGPL-3.0-only"
    }
  },
  "servers": [
    {
      "url": "https://farsight.r2d2.office.ilab.zone",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "system",
      "description": "Process liveness, readiness, metrics, and contract / reference endpoints."
    },
    {
      "name": "catalog",
      "description": "Feed catalog, dataset, and layer discovery — the entry points used by clients to walk the available data surface."
    },
    {
      "name": "tiles",
      "description": "Vector tiles — MapLibre GL `vector` source; replaces whole-collection fetches."
    },
    {
      "name": "streaming",
      "description": "Live delta streaming over the Zenoh spine — SSE, WebSocket, and gRPC `StreamLayer`."
    },
    {
      "name": "history",
      "description": "History & replay backed by QuestDB."
    },
    {
      "name": "weather",
      "description": "Weather. `/weather` samples a viewport grid into GeoJSON points; `/v1/{endpoint}` (also served as `/openmeteo/v1/{endpoint}`) forwards Open-Meteo's own request shape to the configured upstream and caches the answer, so a client written against Open-Meteo works unchanged against Farsight. Upstream status codes are preserved, including 429."
    },
    {
      "name": "mqtt",
      "description": "MQTT output bindings (FEEDS-wby.8). Global config + per-feed bindings drive the egress engine: live feeds firehose, static feeds retained or pulsed, with per-feed broker/credential overrides (secret refs) and topic templating. Admin-gated (X-Operator-Id; optional MQTT_ADMIN_OPERATOR) and audited. Bindings apply on next boot (boot-time apply); PUT/POST validate + persist immediately. No broker configured ⇒ egress inactive (graceful)."
    }
  ],
  "paths": {
    "/healthz": {
      "get": {
        "operationId": "getHealthz",
        "tags": [
          "system"
        ],
        "summary": "Liveness probe.",
        "description": "Returns 200 once the process has bound its listener. Used by Kubernetes liveness checks; no upstream calls are made.",
        "responses": {
          "200": {
            "description": "Process is alive.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthStatus"
                },
                "example": {
                  "status": "ok"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/readyz": {
      "get": {
        "operationId": "getReadyz",
        "tags": [
          "system"
        ],
        "summary": "Readiness probe with layer + dataset counts.",
        "description": "Returns 200 once the catalog has loaded and at least one layer is registered; 503 while still warming. Used by Kubernetes readiness checks.",
        "responses": {
          "200": {
            "description": "Process is ready to serve traffic.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReadyStatus"
                },
                "example": {
                  "status": "ready",
                  "layers": 42,
                  "datasets": 8
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/metrics": {
      "get": {
        "operationId": "getMetrics",
        "tags": [
          "system"
        ],
        "summary": "Prometheus metrics.",
        "description": "Prometheus text-exposition format covering HTTP latency, layer hit counts, upstream poll outcomes, and Zenoh delta throughput.",
        "responses": {
          "200": {
            "description": "Prometheus text exposition.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "# HELP feed_layer_features Number of features served per layer\nfeed_layer_features{layer=\"adsb\"} 1837\n"
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/docs": {
      "get": {
        "operationId": "getDocsHome",
        "tags": [
          "system"
        ],
        "summary": "Fumadocs documentation home, served by the sidecar runtime.",
        "description": "Reverse-proxied to the docs sidecar; returns the rendered Fumadocs landing page.",
        "responses": {
          "200": {
            "description": "HTML documentation page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                },
                "example": "<!doctype html><html><head><title>Farsight Feeds Docs</title>…</html>"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/docs/{slug}": {
      "get": {
        "operationId": "getDocsPage",
        "tags": [
          "system"
        ],
        "summary": "Fumadocs documentation page, served by the sidecar runtime.",
        "description": "Reverse-proxied to the docs sidecar; returns the rendered Fumadocs page for the given slug.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Page slug under the /docs tree.",
            "schema": {
              "type": "string",
              "example": "architecture"
            },
            "examples": {
              "api": {
                "value": "api"
              },
              "architecture": {
                "value": "architecture"
              },
              "feedReference": {
                "value": "feed-reference"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "HTML documentation page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                },
                "example": "<!doctype html><html><head><title>Architecture · Farsight Feeds</title>…</html>"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/openapi.yaml": {
      "get": {
        "operationId": "getOpenapiYaml",
        "tags": [
          "system"
        ],
        "summary": "OpenAPI 3.1 contract as YAML.",
        "description": "This document, served as YAML. Identical content to /openapi.json.",
        "responses": {
          "200": {
            "description": "OpenAPI YAML document.",
            "content": {
              "application/yaml": {
                "schema": {
                  "type": "string"
                },
                "example": "openapi: 3.1.0\ninfo:\n  title: Farsight Feeds API\n  version: 1.0.0\n"
              }
            }
          },
          "500": {
            "description": "OpenAPI document could not be serialised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenapiJson",
        "tags": [
          "system"
        ],
        "summary": "OpenAPI 3.1 contract as JSON.",
        "description": "This document, served as JSON. Identical content to /openapi.yaml. Consumed by the Scalar reference at /reference.",
        "responses": {
          "200": {
            "description": "OpenAPI JSON document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "openapi": "3.1.0",
                  "info": {
                    "title": "Farsight Feeds API",
                    "version": "1.0.0"
                  }
                }
              }
            }
          },
          "500": {
            "description": "OpenAPI document could not be serialised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/scalar": {
      "get": {
        "operationId": "getScalarReference",
        "tags": [
          "system"
        ],
        "summary": "Scalar API reference loaded from /openapi.json.",
        "description": "Themed Scalar UI rendering this OpenAPI contract. Try-it requests hit the production server URL.",
        "responses": {
          "200": {
            "description": "HTML API reference.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                },
                "example": "<!doctype html><html><head><title>Farsight Feeds API Reference</title>…</html>"
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/reference": {
      "get": {
        "operationId": "getReference",
        "tags": [
          "system"
        ],
        "summary": "Alias for the Scalar API reference.",
        "description": "302/200 to the Scalar UI; the canonical public reference URL.",
        "responses": {
          "200": {
            "description": "HTML API reference.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                },
                "example": "<!doctype html><html><head><title>Farsight Feeds API Reference</title>…</html>"
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/farsight/3d": {
      "get": {
        "operationId": "getFarsight3d",
        "tags": [
          "system"
        ],
        "summary": "3D globe for the render contract.",
        "description": "Static HTML for the CesiumJS 3D viewer that consumes /layers and /tiles. Companion to /farsight (2D).",
        "responses": {
          "200": {
            "description": "HTML 3D map.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                },
                "example": "<!doctype html><html><head><title>Farsight 3D</title>…</html>"
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/weather": {
      "get": {
        "operationId": "getWeatherGrid",
        "tags": [
          "weather"
        ],
        "summary": "Wind and temperature sampled over a viewport grid.",
        "description": "Fans a single bulk forecast request out over a lon/lat grid spanning the requested viewport and returns one GeoJSON Point per sample, carrying wind speed (m/s), direction, temperature, and the u/v components a particle renderer wants. Backed by the configured Open-Meteo upstream (`OPENMETEO_URL`).",
        "parameters": [
          {
            "name": "bbox",
            "in": "query",
            "description": "Viewport as `minLon,minLat,maxLon,maxLat`. Defaults to a near-global view.",
            "schema": {
              "type": "string",
              "example": "-10,35,30,60"
            }
          },
          {
            "name": "grid",
            "in": "query",
            "description": "Sample density as `COLSxROWS`. Default `14x9`; each axis is clamped to 2..40.",
            "schema": {
              "type": "string",
              "example": "20x12"
            }
          },
          {
            "name": "time",
            "in": "query",
            "description": "Unix seconds of the forecast hour to sample. Defaults to now.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON FeatureCollection of sampled points.",
            "content": {
              "application/geo+json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Malformed bbox.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "502": {
            "description": "The Open-Meteo upstream failed or returned a non-JSON body.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/{endpoint}": {
      "get": {
        "operationId": "getOpenMeteoPassthrough",
        "tags": [
          "weather"
        ],
        "summary": "Open-Meteo passthrough with a shared response cache.",
        "description": "Forwards Open-Meteo's own request shape to the configured upstream and caches successful answers (default 10 minutes), so many map clients panning a wind or wave layer cost the upstream one request per distinct query. The query string is forwarded verbatim — parameter semantics are Open-Meteo's, documented at https://open-meteo.com/en/docs — and the upstream status code, including 429, is preserved so client back-off logic keeps working. Responses carry `X-Farsight-Cache: HIT|MISS`. Also served at `/openmeteo/v1/{endpoint}` for clients whose proxy forwards the prefix.",
        "parameters": [
          {
            "name": "endpoint",
            "in": "path",
            "required": true,
            "description": "The Open-Meteo API to call.",
            "schema": {
              "type": "string",
              "enum": [
                "forecast",
                "marine",
                "air-quality",
                "ensemble",
                "archive",
                "elevation",
                "flood",
                "climate",
                "seasonal"
              ],
              "example": "forecast"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The upstream response, unmodified.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Endpoint not in the allow-list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "429": {
            "description": "Upstream throttled the request; passed through unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "502": {
            "description": "Upstream unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "504": {
            "description": "Upstream did not answer within the request budget.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/openmeteo/v1/{endpoint}": {
      "get": {
        "operationId": "getOpenMeteoPassthroughPrefixed",
        "tags": [
          "weather"
        ],
        "summary": "Open-Meteo passthrough (prefixed spelling).",
        "description": "Identical to `/v1/{endpoint}`. Both exist so a consumer can point at Farsight whether or not its own reverse proxy strips a `/openmeteo/` prefix before forwarding.",
        "parameters": [
          {
            "name": "endpoint",
            "in": "path",
            "required": true,
            "description": "The Open-Meteo API to call.",
            "schema": {
              "type": "string",
              "enum": [
                "forecast",
                "marine",
                "air-quality",
                "ensemble",
                "archive",
                "elevation",
                "flood",
                "climate",
                "seasonal"
              ],
              "example": "forecast"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The upstream response, unmodified.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Endpoint not in the allow-list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "429": {
            "description": "Upstream throttled the request; passed through unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "502": {
            "description": "Upstream unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "504": {
            "description": "Upstream did not answer within the request budget.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/catalog": {
      "get": {
        "summary": "The loaded feeds-catalog.yaml re-rendered as JSON.",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/rendering/3d": {
      "get": {
        "summary": "Renderer-agnostic 3D layer contract and demonstration metadata.",
        "responses": {
          "200": {
            "description": "3D rendering metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string"
                    },
                    "units": {
                      "type": "object"
                    },
                    "propertyFields": {
                      "type": "object"
                    },
                    "layerProfiles": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/layers": {
      "get": {
        "summary": "List every layer key the catalog knows about, with their dataset bindings and variants.",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayerList"
                }
              }
            }
          }
        }
      }
    },
    "/layers/{key}": {
      "get": {
        "summary": "GeoJSON FeatureCollection for one layer.",
        "description": "Returns the layer as a GeoJSON FeatureCollection. Layers may be static\n(baked datasets), polled, or streamed. The live moving-asset layers —\n`adsb` (aircraft, see `AdsbProperties`) and `ais` (vessels, see\n`AisProperties`) — are upstream-fed with no static fallback: they return\nan empty FeatureCollection (HTTP 200) until the first snapshot arrives.\n`ais` is merged from all enabled AIS sources; `properties.source`\nidentifies the provider (`aisstream` / `ais-tcp`).\n",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "examples": {
              "cables": {
                "value": "cables"
              },
              "pipelines": {
                "value": "pipelines"
              },
              "chokepoints": {
                "value": "waterways"
              },
              "adsb": {
                "value": "adsb"
              },
              "ais": {
                "value": "ais"
              },
              "demo3dAssets": {
                "value": "demo3dAssets"
              }
            }
          },
          {
            "name": "geometry",
            "in": "query",
            "required": false,
            "description": "Use `cartographic` to prefer render sidecar geometry when a dataset provides `sidecarFiles.cartographicRender`.",
            "schema": {
              "type": "string",
              "enum": [
                "source",
                "cartographic",
                "render"
              ],
              "default": "source"
            }
          },
          {
            "name": "bbox",
            "in": "query",
            "required": false,
            "description": "Server-side spatial filter `minLon,minLat,maxLon,maxLat` (R-tree). Additive — omitting it returns the full collection.",
            "schema": {
              "type": "string",
              "example": "-10,35,40,70"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Temporal filter — only entities observed at/after this epoch-ms.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "description": "Temporal filter — only entities observed at/before this epoch-ms.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Max features to return; pair with `cursor` for pagination.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor from a previous page (treat as opaque; do not parse).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON FeatureCollection.",
            "content": {
              "application/geo+json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Layer unknown or has no bound dataset."
          }
        }
      }
    },
    "/layers/{key}/history": {
      "get": {
        "tags": [
          "history"
        ],
        "summary": "Decimated whole-layer position history (QuestDB).",
        "description": "Time-ordered, SAMPLE-BY-decimated history for a firehose layer, sourced\nfrom QuestDB (FEEDS-m0f.6). Only `adsb` and `ais` are streamed to QuestDB;\nany other layer returns 404. One GeoJSON Point feature per sampled fix per\nentity (`last(...)` value in each SAMPLE BY bucket); `properties` carry the\nentity id and the bucket `timestamp`. Row count is capped server-side.\n\nGraceful degradation: when QuestDB is not configured the route returns\n`503` with `{ \"error\": ... }` (never 500/panic), mirroring the write sink.\n",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "adsb",
                "ais"
              ]
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Window start, epoch-ms. Defaults to one hour before `until`.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "description": "Window end, epoch-ms. Defaults to now.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "sample",
            "in": "query",
            "required": false,
            "description": "QuestDB SAMPLE BY interval (`^\\d+[smhdMy]$`), e.g. `30s`, `1m`, `5m`.",
            "schema": {
              "type": "string",
              "default": "1m",
              "example": "30s"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Time-ordered GeoJSON FeatureCollection of sampled fixes.",
            "content": {
              "application/geo+json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid sample interval."
          },
          "404": {
            "description": "Layer has no QuestDB history (not a firehose layer)."
          },
          "503": {
            "description": "QuestDB history not configured or unreachable."
          }
        }
      }
    },
    "/entities/{layer}/{id}/track": {
      "get": {
        "tags": [
          "history"
        ],
        "summary": "One entity's ordered track with ASOF-JOIN enrichment (QuestDB).",
        "description": "Ordered track for a single entity, sourced from QuestDB (FEEDS-m0f.6).\nReturns a GeoJSON FeatureCollection of Point features ordered by time, each\nproperty bag carrying the fix `timestamp` (so a replay timeline can consume\npoints + timestamps directly). The QuestDB query carries the slowly-changing\nidentity attribute forward via an ASOF JOIN (`nav_status` for `ais`,\n`callsign` for `adsb`); reference dimensions (registration / vessel name,\ntype, flag …) are spliced handler-side as `properties.aircraft` /\n`properties.vessel`. Optional `sample` decimates the track.\n",
        "parameters": [
          {
            "name": "layer",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "adsb",
                "ais"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Entity id — decimal MMSI for `ais`, hex ICAO24 (1-6 chars) for `adsb`.",
            "schema": {
              "type": "string",
              "example": "366123456"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "sample",
            "in": "query",
            "required": false,
            "description": "Optional SAMPLE BY interval (`^\\d+[smhdMy]$`) to decimate the track.",
            "schema": {
              "type": "string",
              "example": "30s"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ordered GeoJSON FeatureCollection of track points.",
            "content": {
              "application/geo+json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Invalid entity id or sample interval."
          },
          "404": {
            "description": "Layer has no QuestDB history (not a firehose layer)."
          },
          "503": {
            "description": "QuestDB history not configured or unreachable."
          }
        }
      }
    },
    "/replay/{key}": {
      "get": {
        "tags": [
          "history"
        ],
        "summary": "Replay session descriptor over real recorded time (QuestDB).",
        "description": "A replay SESSION DESCRIPTOR (not frames) for a firehose layer (FEEDS-m0f.6).\nReports the REAL recorded time bounds straight from QuestDB\n(`min`/`max`/`count` over the window) so the client never fabricates\ntimestamps. When the window has no rows, `has_data` is false and\n`actual_start`/`actual_end` are null. The client plays decimated slices\n(via `/layers/{key}/history`) with dead-reckoning interpolation.\n",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "adsb",
                "ais"
              ]
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "speed",
            "in": "query",
            "required": false,
            "description": "Playback speed multiplier (echoed back; client-side concern).",
            "schema": {
              "type": "number",
              "default": 1.0
            }
          },
          {
            "name": "sample",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "1m"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Replay descriptor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReplayDescriptor"
                }
              }
            }
          },
          "400": {
            "description": "Invalid sample interval."
          },
          "404": {
            "description": "Layer has no QuestDB history (not a firehose layer)."
          },
          "503": {
            "description": "QuestDB history not configured or unreachable."
          }
        }
      }
    },
    "/alerts": {
      "get": {
        "summary": "Normalized, filterable feed of event-type items (real-time / historical awareness).",
        "description": "Unifies time-stamped occurrences across event-like layers (conflict\nevents, attacks, natural disasters, fires, weather, GDELT hotspots, …)\ninto a flat, normalized list. Static infrastructure layers (cables,\npipelines, bases) are intentionally excluded. Every query param is an\nadditive filter — the URI *is* the filter. Items are sorted newest-first.\nNothing is fabricated: an alert exists only if a real feed feature\nproduced it. \"Live\" awareness is `since=<recent window>`; historical is\nan explicit `since`/`until` range.\n",
        "parameters": [
          {
            "name": "types",
            "in": "query",
            "required": false,
            "description": "Comma-separated normalized types (see `/alerts/facets`).",
            "schema": {
              "type": "string",
              "example": "conflict,natural"
            }
          },
          {
            "name": "sources",
            "in": "query",
            "required": false,
            "description": "Comma-separated source-label substrings (case-insensitive).",
            "schema": {
              "type": "string",
              "example": "GDELT,USGS"
            }
          },
          {
            "name": "severity",
            "in": "query",
            "required": false,
            "description": "Comma-separated severities.",
            "schema": {
              "type": "string",
              "example": "high,medium"
            }
          },
          {
            "name": "layers",
            "in": "query",
            "required": false,
            "description": "Restrict to specific event-layer keys.",
            "schema": {
              "type": "string",
              "example": "ucdpEvents,natural"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text match over title + summary.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Lower time bound — RFC3339 instant or relative duration (`24h`, `7d`, `90m`).",
            "schema": {
              "type": "string",
              "example": "24h"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "description": "Upper time bound — RFC3339 instant or relative duration.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bbox",
            "in": "query",
            "required": false,
            "description": "Spatial filter `minLon,minLat,maxLon,maxLat`.",
            "schema": {
              "type": "string",
              "example": "-10,35,40,70"
            }
          },
          {
            "name": "aoi",
            "in": "query",
            "required": false,
            "description": "Comma-separated AOI ids (union) from the operator control plane.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Max items to return (default 200, max 2000).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 2000,
              "default": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Filtered, paginated alert list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid filter value (e.g. malformed bbox, unparseable since/until, unknown AOI)."
          }
        }
      }
    },
    "/alerts/facets": {
      "get": {
        "summary": "Available alert types, sources, severities, and layers with counts.",
        "description": "Powers filter UIs and discoverable `/alerts` URIs. No filters applied.",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertFacets"
                }
              }
            }
          }
        }
      }
    },
    "/fusion/{layer}": {
      "get": {
        "summary": "Correlation and fusion report for point observations in a layer.",
        "description": "Reads the current GeoJSON layer snapshot, identifies nearby point\nobservations that are statistically compatible, and returns a report of\ndeduplication/stitching candidates plus fused positions. The source\nlayer is not modified. Weighted variance, standard deviation, and\nstandard error use normalized inverse-variance weights with the Zaiontz\ncorrection form.\n",
        "parameters": [
          {
            "name": "layer",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "ais"
            }
          },
          {
            "name": "maxDistanceM",
            "in": "query",
            "required": false,
            "description": "Max physical separation to consider for pairwise correlation.",
            "schema": {
              "type": "number",
              "default": 150,
              "minimum": 0
            }
          },
          {
            "name": "alpha",
            "in": "query",
            "required": false,
            "description": "Minimum two-sided similarity probability for a statistical correlation.",
            "schema": {
              "type": "number",
              "default": 0.05,
              "minimum": 0,
              "maximum": 1
            }
          },
          {
            "name": "defaultStdErrorM",
            "in": "query",
            "required": false,
            "description": "Standard error used when a feature lacks uncertainty fields.",
            "schema": {
              "type": "number",
              "default": 25,
              "minimum": 0
            }
          },
          {
            "name": "includeSingletons",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "includePairs",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "pairLimit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 200,
              "minimum": 1,
              "maximum": 2000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fusion/correlation report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FusionReport"
                }
              }
            }
          },
          "400": {
            "description": "Invalid fusion parameter."
          },
          "404": {
            "description": "Layer unknown or has no bound dataset/source."
          }
        }
      }
    },
    "/datasets": {
      "get": {
        "summary": "List every registered dataset name and its catalog metadata.",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/datasets/{name}": {
      "get": {
        "summary": "Return one catalog or enrichment dataset in its native JSON shape.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "examples": {
              "cables": {
                "value": "underseaCables"
              },
              "pipelines": {
                "value": "pipelines"
              },
              "chokepoints": {
                "value": "chokepoints"
              },
              "aircraft": {
                "value": "aircraft_data"
              },
              "ships": {
                "value": "ships_data"
              },
              "spacecraft": {
                "value": "spacecraft_data"
              }
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Optional page size for dataset previews and enrichment table reads.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Optional zero-based record offset. If supplied without `limit`, the API uses the maximum page size.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Raw dataset data (array or object), optionally paged."
          }
        }
      }
    },
    "/sat/search": {
      "get": {
        "summary": "Search the spacecraft catalog by NORAD id, international designator, name, operator, or constellation.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "examples": {
              "byName": {
                "value": "ISS"
              },
              "byNorad": {
                "value": "25544"
              },
              "byGroup": {
                "value": "starlink"
              }
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching catalog rows (compact)."
          },
          "400": {
            "description": "Missing query."
          }
        }
      }
    },
    "/sat/stats": {
      "get": {
        "summary": "Catalog size, TLE coverage, object-type breakdown, and last sync time.",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/sat/{id}": {
      "get": {
        "summary": "Full detail for one object — identity, orbital elements, live position (geodetic + ECI + ECEF), and TLE if present.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "examples": {
              "iss": {
                "value": "25544"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          },
          "404": {
            "description": "Unknown NORAD id."
          }
        }
      }
    },
    "/sat/{id}/track": {
      "get": {
        "summary": "Ground track over one orbital period as a GeoJSON LineString.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "samples",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 120,
              "minimum": 8,
              "maximum": 512
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GeoJSON Feature (LineString)."
          },
          "404": {
            "description": "Unknown NORAD id."
          },
          "422": {
            "description": "No propagatable orbit."
          }
        }
      }
    },
    "/sat/{id}/passes": {
      "get": {
        "summary": "Visibility passes over a ground observer (AOS/LOS/TCA, max elevation, azimuths, range).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "alt",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "default": 0
            }
          },
          {
            "name": "hours",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 48,
              "minimum": 1,
              "maximum": 168
            }
          },
          {
            "name": "minElev",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          },
          "400": {
            "description": "Missing/invalid observer lat/lon."
          },
          "404": {
            "description": "Unknown NORAD id."
          }
        }
      }
    },
    "/feeds": {
      "get": {
        "summary": "RSS feed catalogue (CANONICAL_FEEDS plus tier/risk metadata).",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/upstream": {
      "get": {
        "summary": "Configured upstream sources with last-poll status.",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/upstream/{id}/refresh": {
      "post": {
        "summary": "Force-refresh one upstream source now.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "examples": {
              "usgs": {
                "value": "usgs-earthquakes"
              },
              "eonet": {
                "value": "nasa-eonet"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "refreshed"
          },
          "502": {
            "description": "upstream fetch failed"
          }
        }
      }
    },
    "/tiles/{layer}/{z}/{x}/{y}.mvt": {
      "get": {
        "tags": [
          "tiles"
        ],
        "summary": "Mapbox Vector Tile for a layer.",
        "description": "In-process MVT encoded from the pod's `rstar` R-tree over the hot cache — zero DB round-trip. Per-tile `ETag` + `Cache-Control` for edge/CDN caching; deltas invalidate only touched tiles. Replaces fetching whole FeatureCollections. With `?at=<epoch_ms>` the tile is rendered from QuestDB history instead of live state.",
        "parameters": [
          {
            "name": "layer",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "z",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "x",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "y",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "at",
            "in": "query",
            "required": false,
            "description": "Epoch-ms; render a historical tile from QuestDB instead of live state.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Mapbox Vector Tile.",
            "content": {
              "application/vnd.mapbox-vector-tile": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "204": {
            "description": "Empty tile (no entities in this tile)."
          },
          "404": {
            "description": "Layer unknown."
          }
        }
      }
    },
    "/stream/{layer}": {
      "get": {
        "tags": [
          "streaming"
        ],
        "summary": "Server-Sent Events stream of Delta JSON for a layer.",
        "description": "`text/event-stream`; a thin adapter over a Zenoh subscription on `feeds/<mode>/<layer>/**`. The first event is a `Snapshot` so the client starts consistent, then per-entity `Upsert`/`Remove` deltas follow.",
        "parameters": [
          {
            "name": "layer",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "SSE stream; each `data:` frame is a Delta JSON object.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "$ref": "#/components/schemas/Delta"
                }
              }
            }
          }
        }
      }
    },
    "/ws/{layer}": {
      "get": {
        "tags": [
          "streaming"
        ],
        "summary": "WebSocket of CBOR/JSON Delta frames for a layer.",
        "description": "WebSocket upgrade. Client sends `{action:\"subscribe\", layer, bbox?}` / `{action:\"unsubscribe\", layer}`; the server replies with an initial `Snapshot`, then pushes `Delta` frames, with a heartbeat every N seconds. `bbox` on subscribe applies server-side viewport culling. Clients reconnect with exponential backoff.",
        "parameters": [
          {
            "name": "layer",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "101": {
            "description": "Switching Protocols (WebSocket)."
          }
        }
      }
    },
    "/catalog/feeds": {
      "get": {
        "tags": [
          "catalog"
        ],
        "summary": "Marketplace roster — cards for the grid.",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter by category tab."
          },
          {
            "name": "trust",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "official",
                "verified",
                "community",
                "experimental"
              ]
            },
            "description": "Filter by trust badge."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Free-text search."
          }
        ],
        "responses": {
          "200": {
            "description": "Feed cards.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CatalogFeed"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/catalog/categories": {
      "get": {
        "tags": [
          "catalog"
        ],
        "summary": "Category tab list with counts.",
        "responses": {
          "200": {
            "description": "Categories + counts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "category": {
                        "type": "string"
                      },
                      "count": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/me/feeds": {
      "get": {
        "tags": [
          "subscriptions"
        ],
        "summary": "The calling operator's installed roster. Requires operator auth.",
        "responses": {
          "200": {
            "description": "Installed feed cards.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CatalogFeed"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/me/feeds/{feedId}": {
      "post": {
        "tags": [
          "subscriptions"
        ],
        "summary": "Install (subscribe) — idempotent. Mutates operator_subscription only; no ingestion side-effects.",
        "parameters": [
          {
            "name": "feedId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Subscribed."
          }
        }
      },
      "delete": {
        "tags": [
          "subscriptions"
        ],
        "summary": "Uninstall (unsubscribe).",
        "parameters": [
          {
            "name": "feedId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Unsubscribed."
          }
        }
      },
      "patch": {
        "tags": [
          "subscriptions"
        ],
        "summary": "Per-operator display settings (color, default visible…).",
        "parameters": [
          {
            "name": "feedId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated."
          }
        }
      }
    },
    "/custom": {
      "get": {
        "tags": [
          "custom-feeds"
        ],
        "summary": "List my custom feeds. Owner auth required.",
        "responses": {
          "200": {
            "description": "Custom feed configs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "custom-feeds"
        ],
        "summary": "Create a custom feed. Owner auth required.",
        "requestBody": {
          "required": true,
          "description": "CustomFeedConfig — see formats.md.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created — returns the id and the capability URL (the link is the credential).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/custom/{id}/config": {
      "get": {
        "tags": [
          "custom-feeds"
        ],
        "summary": "Read a custom feed's config. Owner auth required.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "CustomFeedConfig.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Unknown id."
          }
        }
      }
    },
    "/custom/{id}": {
      "get": {
        "tags": [
          "custom-feeds"
        ],
        "summary": "Consume a custom feed (capability URL — the link is the credential).",
        "description": "Resolves the union of each config feed's filtered `GeoEntity` set (operator-control-plane filters) through the formatter registry. Content negotiation: `?type=` wins, else `Accept`, else the feed's `default_type`. Saved filters intersect with request-time `bbox`/`since`/ `until`/`limit`. `mvt` rides the in-process R-tree tiler (FEEDS-m0f).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "geojson",
                "mvt",
                "cot",
                "oth",
                "ndjson"
              ]
            },
            "description": "Output format; overrides Accept. Unknown ⇒ 415."
          },
          {
            "name": "bbox",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "minLon,minLat,maxLon,maxLat"
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "z",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "MVT only (or use the .mvt path)."
          },
          {
            "name": "x",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "y",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feed in the negotiated format.",
            "content": {
              "application/geo+json": {
                "schema": {
                  "type": "object"
                }
              },
              "application/vnd.mapbox-vector-tile": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              },
              "application/x-ndjson": {
                "schema": {
                  "$ref": "#/components/schemas/GeoEntity"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown id."
          },
          "410": {
            "description": "Revoked or expired (enforced at Envoy + resolver)."
          },
          "415": {
            "description": "Unknown type."
          },
          "429": {
            "description": "Rate-limited (Envoy per-id)."
          }
        }
      },
      "put": {
        "tags": [
          "custom-feeds"
        ],
        "summary": "Update a custom feed's config. Owner auth required.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated."
          }
        }
      },
      "delete": {
        "tags": [
          "custom-feeds"
        ],
        "summary": "Revoke a custom feed (invalidates the capability URL). Owner auth required.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          }
        }
      }
    },
    "/custom/{id}/rotate": {
      "post": {
        "tags": [
          "custom-feeds"
        ],
        "summary": "Rotate the capability id, invalidating the old URL. Owner auth required.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "New capability URL.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/custom/{id}/{z}/{x}/{y}.mvt": {
      "get": {
        "tags": [
          "custom-feeds"
        ],
        "summary": "Vector tile for a custom feed (path form of `?type=mvt`).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "z",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "x",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "y",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Mapbox Vector Tile.",
            "content": {
              "application/vnd.mapbox-vector-tile": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "204": {
            "description": "Empty tile."
          },
          "410": {
            "description": "Revoked or expired."
          }
        }
      }
    },
    "/custom/{id}/stream": {
      "get": {
        "tags": [
          "custom-feeds",
          "streaming"
        ],
        "summary": "SSE deltas of the composed custom feed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "SSE stream of Delta JSON.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "$ref": "#/components/schemas/Delta"
                }
              }
            }
          }
        }
      }
    },
    "/custom/{id}/ws": {
      "get": {
        "tags": [
          "custom-feeds",
          "streaming"
        ],
        "summary": "WebSocket deltas of the composed custom feed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "101": {
            "description": "Switching Protocols (WebSocket)."
          }
        }
      }
    },
    "/mqtt/config": {
      "get": {
        "tags": [
          "mqtt"
        ],
        "summary": "Global MQTT output config (admin).",
        "responses": {
          "200": {
            "description": "Global broker/base/defaults.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MqttConfig"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "mqtt"
        ],
        "summary": "Set global MQTT broker/base/defaults (admin; audited).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MqttConfig"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated config (resolved view).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MqttConfig"
                }
              }
            }
          },
          "400": {
            "description": "Invalid version or default_qos."
          },
          "403": {
            "description": "Operator not permitted (MQTT_ADMIN_OPERATOR set)."
          }
        }
      }
    },
    "/catalog/feeds/{id}/mqtt": {
      "get": {
        "tags": [
          "mqtt"
        ],
        "summary": "Resolved MQTT binding (global + per-feed override) for a catalog feed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved binding.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResolvedMqttBinding"
                }
              }
            }
          },
          "404": {
            "description": "Unknown feed."
          }
        }
      },
      "put": {
        "tags": [
          "mqtt"
        ],
        "summary": "Set/override the MQTT binding for a catalog feed (admin; audited).",
        "description": "Validates the topic template (rejects MQTT wildcards in literals, unknown variables, and over-cardinality geohash precision) before persisting. Applies on next boot.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedMqttBinding"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bound (resolved binding + appliesOn marker).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "binding": {
                      "$ref": "#/components/schemas/ResolvedMqttBinding"
                    },
                    "appliesOn": {
                      "type": "string",
                      "example": "next-boot"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid topic template / qos / pulse interval."
          },
          "403": {
            "description": "Operator not permitted."
          },
          "404": {
            "description": "Unknown feed."
          }
        }
      }
    },
    "/custom/{id}/mqtt": {
      "post": {
        "tags": [
          "mqtt"
        ],
        "summary": "Bind a custom feed to MQTT (admin; audited).",
        "description": "Binds a custom (capability-id) feed as an MQTT output. Default topic is `{base}/custom/<id>/{id}`. Same delivery/QoS/retain/broker/credential options as catalog feeds. Applies on next boot.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedMqttBinding"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bound (resolved binding + appliesOn marker).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "binding": {
                      "$ref": "#/components/schemas/ResolvedMqttBinding"
                    },
                    "appliesOn": {
                      "type": "string",
                      "example": "next-boot"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid topic template / qos / pulse interval."
          },
          "403": {
            "description": "Operator not permitted."
          },
          "404": {
            "description": "Unknown custom feed."
          }
        }
      }
    },
    "/v1/feed-manager/summary": {
      "get": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Counts by mode/enabled-state plus adapter count.",
        "responses": {
          "200": {
            "description": "summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponse"
                }
              }
            }
          },
          "404": {
            "description": "feature disabled"
          }
        }
      }
    },
    "/v1/feed-manager/adapters": {
      "get": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Adapter registry descriptors.",
        "responses": {
          "200": {
            "description": "adapters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/feed-manager/feeds": {
      "get": {
        "tags": [
          "feed-manager"
        ],
        "summary": "List feed definitions.",
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "fixed",
                "dynamic",
                "stored",
                "cached",
                "live"
              ]
            }
          },
          {
            "name": "enabled",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "layer",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "feed list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Create a feed (id optional — generated from name).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedDefinition"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponse"
                }
              }
            }
          },
          "422": {
            "description": "validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponse"
                }
              }
            }
          },
          "409": {
            "description": "id already exists"
          }
        }
      }
    },
    "/v1/feed-manager/feeds/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Get one feed definition.",
        "responses": {
          "200": {
            "description": "feed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponse"
                }
              }
            }
          },
          "404": {
            "description": "not found"
          }
        }
      },
      "put": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Update a feed (optionally optimistic-locked).",
        "parameters": [
          {
            "name": "expectedVersion",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedDefinition"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "updated"
          },
          "409": {
            "description": "version conflict"
          },
          "422": {
            "description": "validation error"
          }
        }
      },
      "delete": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Delete a feed.",
        "responses": {
          "200": {
            "description": "deleted"
          },
          "404": {
            "description": "not found"
          }
        }
      }
    },
    "/v1/feed-manager/feeds/{id}/validate": {
      "post": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Dry-run validation (never persists).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedDefinition"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "valid"
          },
          "422": {
            "description": "validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/feed-manager/feeds/{id}/enable": {
      "post": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Enable a feed (rejects invalid feeds with invalid_state).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "enabled"
          },
          "409": {
            "description": "invalid_state"
          }
        }
      }
    },
    "/v1/feed-manager/feeds/{id}/disable": {
      "post": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Disable a feed (stops scheduling).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "disabled"
          }
        }
      }
    },
    "/v1/feed-manager/feeds/{id}/refresh": {
      "post": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Force a refresh (invalid_state for fixed feeds).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "refreshed"
          },
          "409": {
            "description": "invalid_state"
          }
        }
      }
    },
    "/v1/feed-manager/feeds/{id}/runs": {
      "get": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Run history for a feed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "runs"
          }
        }
      }
    },
    "/v1/feed-manager/feeds/{id}/snapshots": {
      "get": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Snapshot history for a feed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "snapshots"
          }
        }
      }
    },
    "/v1/feed-manager/feeds/{id}/audit": {
      "get": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Audit events for one feed (newest first).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "audit events"
          }
        }
      }
    },
    "/v1/feed-manager/audit": {
      "get": {
        "tags": [
          "feed-manager"
        ],
        "summary": "Global audit log (newest first).",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "audit events"
          }
        }
      }
    }
  },
  "components": {
    "responses": {
      "BadRequest": {
        "description": "Malformed request — invalid query parameters, body, or path.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "bad_request",
              "message": "bbox must be 4 comma-separated numbers"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid credentials.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "unauthorized",
              "message": "operator id required"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Caller lacks permission for this operation.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "forbidden",
              "message": "operator not allowed to write mqtt config"
            }
          }
        }
      },
      "NotFound": {
        "description": "The named resource does not exist.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "not_found",
              "message": "layer 'fictional' not found"
            }
          }
        }
      },
      "Gone": {
        "description": "The resource existed but has been retired.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "gone",
              "message": "custom feed expired"
            }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "Request body media type is not supported by this operation.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "unsupported_media_type",
              "message": "expected application/json"
            }
          }
        }
      },
      "Unprocessable": {
        "description": "Request was well-formed but failed validation rules.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "unprocessable_entity",
              "message": "feed config missing required field 'kind'"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limit exceeded; retry after the indicated window.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "too_many_requests",
              "message": "upstream refresh quota exhausted; retry in 60s"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "Service is not ready or a critical dependency is unavailable.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "not_ready",
              "message": "catalog still loading"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Stable error envelope returned by every non-2xx JSON response.",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable error code in snake_case (e.g. not_found, unprocessable_entity)."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation; safe to surface to operators."
          },
          "details": {
            "type": "object",
            "nullable": true,
            "description": "Optional structured payload (validation errors, conflicting state, etc.)."
          }
        }
      },
      "HealthStatus": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ],
            "example": "ok"
          }
        }
      },
      "ReadyStatus": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ready",
              "warming"
            ],
            "example": "ready"
          },
          "layers": {
            "type": "integer",
            "description": "Number of layers registered in the catalog."
          },
          "datasets": {
            "type": "integer",
            "description": "Number of distinct datasets loaded."
          }
        }
      },
      "MqttConfig": {
        "type": "object",
        "description": "Global MQTT output config (the `mqtt:` block). Credential is a SECRET REF (`${ENV}`), never inline.",
        "properties": {
          "broker": {
            "type": "string",
            "nullable": true,
            "description": "Global default broker, e.g. tls://broker:8883 or mqtt://localhost:1883. null ⇒ egress inactive unless a per-feed binding supplies one."
          },
          "version": {
            "type": "integer",
            "enum": [
              3,
              4,
              5
            ],
            "default": 5
          },
          "baseTopic": {
            "type": "string",
            "default": "farsight",
            "description": "Topic namespace root (the {base} template var)."
          },
          "defaultQos": {
            "type": "integer",
            "enum": [
              0,
              1,
              2
            ],
            "default": 0
          },
          "retainStatic": {
            "type": "boolean",
            "default": true
          },
          "credential": {
            "type": "string",
            "nullable": true,
            "description": "Secret ref like ${MQTT_GLOBAL_CRED}, resolved from env at connect time."
          },
          "active": {
            "type": "boolean",
            "readOnly": true,
            "description": "True when a broker is configured (response only)."
          }
        }
      },
      "FeedMqttBinding": {
        "type": "object",
        "description": "Per-feed binding. Unset fields inherit global defaults + the feed's catalog mode (live→firehose, static→retained, hybrid→firehose).",
        "properties": {
          "feedId": {
            "type": "string",
            "description": "Authoritative from the path; body value is ignored."
          },
          "enabled": {
            "type": "boolean",
            "default": true
          },
          "delivery": {
            "type": "string",
            "enum": [
              "firehose",
              "retained",
              "pulsed"
            ],
            "nullable": true
          },
          "qos": {
            "type": "integer",
            "enum": [
              0,
              1,
              2
            ],
            "nullable": true
          },
          "retain": {
            "type": "boolean",
            "nullable": true
          },
          "topicTemplate": {
            "type": "string",
            "nullable": true,
            "description": "e.g. {base}/maritime/ais/{msgType}/{region}/{id}. Vars: {base}{category}{feed}{id}{geohashN}; {msgType}/{region}/{nai}/{waterway} are wby.9 spatial tags (resolved from properties, else 'unknown')."
          },
          "pulseIntervalMs": {
            "type": "integer",
            "nullable": true,
            "description": "Re-publish cadence for pulsed delivery (≥1000)."
          },
          "broker": {
            "type": "string",
            "nullable": true,
            "description": "Per-feed broker override. null ⇒ global."
          },
          "credential": {
            "type": "string",
            "nullable": true,
            "description": "Per-feed credential secret ref. null ⇒ global."
          }
        }
      },
      "ResolvedMqttBinding": {
        "type": "object",
        "description": "Effective binding = global defaults overlaid with the per-feed override.",
        "properties": {
          "feedId": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "delivery": {
            "type": "string",
            "enum": [
              "firehose",
              "retained",
              "pulsed"
            ]
          },
          "qos": {
            "type": "integer",
            "enum": [
              0,
              1,
              2
            ]
          },
          "retain": {
            "type": "boolean"
          },
          "topicTemplate": {
            "type": "string"
          },
          "pulseIntervalMs": {
            "type": "integer"
          },
          "broker": {
            "type": "string",
            "nullable": true
          },
          "credential": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "LayerList": {
        "type": "object",
        "properties": {
          "layers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "variants": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "datasets": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "geometry": {
                  "type": "string",
                  "nullable": true
                },
                "static_feature_count": {
                  "type": "integer"
                },
                "render3d": {
                  "type": "object"
                }
              }
            }
          },
          "variants": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        }
      },
      "Alert": {
        "type": "object",
        "required": [
          "id",
          "layer",
          "type",
          "source",
          "title",
          "properties"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable per-feature id (layer-prefixed)."
          },
          "layer": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "Normalized event category.",
            "enum": [
              "conflict",
              "attack",
              "geopolitical",
              "unrest",
              "natural",
              "fire",
              "weather",
              "humanitarian",
              "infrastructure",
              "cyber",
              "gps",
              "health"
            ]
          },
          "source": {
            "type": "string",
            "description": "Human-readable origin label."
          },
          "severity": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high"
            ],
            "nullable": true
          },
          "time": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": "string",
            "nullable": true
          },
          "lat": {
            "type": "number",
            "nullable": true
          },
          "lng": {
            "type": "number",
            "nullable": true
          },
          "bbox": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "number"
            },
            "minItems": 4,
            "maxItems": 4
          },
          "properties": {
            "type": "object",
            "description": "Passthrough of the source feature properties."
          }
        }
      },
      "AlertList": {
        "type": "object",
        "required": [
          "alerts",
          "total",
          "returned",
          "generatedAt"
        ],
        "properties": {
          "alerts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Alert"
            }
          },
          "total": {
            "type": "integer",
            "description": "Match count before limit/offset."
          },
          "returned": {
            "type": "integer"
          },
          "filters": {
            "type": "object",
            "description": "Echo of the active filters."
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AlertFacets": {
        "type": "object",
        "properties": {
          "types": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "sources": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "severity": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "layers": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      },
      "FusionReport": {
        "type": "object",
        "required": [
          "layer",
          "generatedAt",
          "parameters",
          "fusedObjects",
          "summary"
        ],
        "properties": {
          "layer": {
            "type": "string"
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "parameters": {
            "type": "object"
          },
          "featuresSeen": {
            "type": "integer"
          },
          "pointMeasurements": {
            "type": "integer"
          },
          "skippedNonPoint": {
            "type": "integer"
          },
          "assumedUncertaintyCount": {
            "type": "integer"
          },
          "unknownIdentityCount": {
            "type": "integer"
          },
          "candidatePairs": {
            "type": "integer"
          },
          "correlatedPairs": {
            "type": "integer"
          },
          "fusedObjects": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "fusedId": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "single_unknown",
                    "single_observation",
                    "unknown_cluster",
                    "same_identity",
                    "identity_conflict"
                  ]
                },
                "memberCount": {
                  "type": "integer"
                },
                "identity": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "identities": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "sources": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "fusedPosition": {
                  "type": "object",
                  "properties": {
                    "lon": {
                      "type": "number"
                    },
                    "lat": {
                      "type": "number"
                    },
                    "varianceM2": {
                      "type": "number"
                    },
                    "standardDeviationM": {
                      "type": "number"
                    },
                    "standardErrorM": {
                      "type": "number"
                    },
                    "confidenceRadius95M": {
                      "type": "number"
                    },
                    "propagatedVarianceFloorM2": {
                      "type": "number"
                    },
                    "axisStats": {
                      "type": "object"
                    }
                  }
                },
                "correlation": {
                  "type": "object"
                },
                "members": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "source": {
                        "type": "string"
                      },
                      "identity": {
                        "type": [
                          "string",
                          "null"
                        ]
                      },
                      "lon": {
                        "type": "number"
                      },
                      "lat": {
                        "type": "number"
                      },
                      "stdErrorM": {
                        "type": "number"
                      },
                      "sampleCount": {
                        "type": "integer"
                      },
                      "weight": {
                        "type": "number"
                      },
                      "errorBasis": {
                        "type": "string"
                      },
                      "observedAt": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      }
                    }
                  }
                }
              }
            }
          },
          "pairs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "a": {
                  "type": "object"
                },
                "b": {
                  "type": "object"
                },
                "distanceM": {
                  "type": "number"
                },
                "totalStdErrorM": {
                  "type": "number"
                },
                "z": {
                  "type": "number"
                },
                "similarity": {
                  "type": "number"
                },
                "correlated": {
                  "type": "boolean"
                },
                "reason": {
                  "type": "string"
                }
              }
            }
          },
          "summary": {
            "type": "object",
            "properties": {
              "reportedObjects": {
                "type": "integer"
              },
              "dedupCandidateGroups": {
                "type": "integer"
              },
              "unknownGroups": {
                "type": "integer"
              },
              "identityConflictGroups": {
                "type": "integer"
              },
              "sameIdentityGroups": {
                "type": "integer"
              }
            }
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "AdsbProperties": {
        "type": "object",
        "description": "`properties` of a feature in the `adsb` layer. Geometry is a Point `[lon, lat, altitudeM?]`. Normalized from OpenSky state vectors.",
        "properties": {
          "dataset": {
            "type": "string",
            "example": "adsb"
          },
          "source": {
            "type": "string",
            "example": "opensky"
          },
          "icao24": {
            "type": "string",
            "description": "24-bit ICAO address (hex)",
            "example": "39de4f"
          },
          "callsign": {
            "type": [
              "string",
              "null"
            ],
            "example": "TVF50AD"
          },
          "originCountry": {
            "type": [
              "string",
              "null"
            ]
          },
          "altitudeM": {
            "type": [
              "number",
              "null"
            ],
            "description": "Geometric altitude (falls back to barometric)",
            "metres": null
          },
          "baroAltitudeM": {
            "type": [
              "number",
              "null"
            ]
          },
          "geoAltitudeM": {
            "type": [
              "number",
              "null"
            ]
          },
          "onGround": {
            "type": "boolean"
          },
          "speedMps": {
            "type": [
              "number",
              "null"
            ],
            "description": "Ground speed",
            "m/s": null
          },
          "headingDeg": {
            "type": [
              "number",
              "null"
            ],
            "description": "True track",
            "degrees": null
          },
          "track": {
            "type": [
              "number",
              "null"
            ],
            "description": "Alias of headingDeg"
          },
          "verticalRate": {
            "type": [
              "number",
              "null"
            ],
            "description": "m/s",
            "+ up": null
          },
          "squawk": {
            "type": [
              "string",
              "null"
            ]
          },
          "military": {
            "type": "boolean",
            "description": "Squawk + callsign heuristic"
          },
          "observedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "freshness": {
            "type": "string",
            "enum": [
              "fresh",
              "stale",
              "expired"
            ]
          },
          "sidc": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reserved for the MIL-STD-2525D side-car (currently null)"
          }
        }
      },
      "AisProperties": {
        "type": "object",
        "description": "`properties` of a feature in the `ais` layer. Geometry is a Point `[lon, lat]`. Merged per-MMSI from one or more AIS sources.",
        "properties": {
          "dataset": {
            "type": "string",
            "example": "ais"
          },
          "source": {
            "type": "string",
            "enum": [
              "aisstream",
              "ais-tcp"
            ]
          },
          "mmsi": {
            "type": "integer",
            "description": "Maritime Mobile Service Identity"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "callsign": {
            "type": [
              "string",
              "null"
            ]
          },
          "headingDeg": {
            "type": [
              "number",
              "null"
            ],
            "description": "True heading (falls back to COG)",
            "degrees": null
          },
          "cog": {
            "type": [
              "number",
              "null"
            ],
            "description": "Course over ground",
            "degrees": null
          },
          "trueHeading": {
            "type": [
              "number",
              "null"
            ]
          },
          "speedMps": {
            "type": [
              "number",
              "null"
            ],
            "description": "Speed over ground",
            "m/s": null
          },
          "sogKnots": {
            "type": [
              "number",
              "null"
            ]
          },
          "navStatus": {
            "type": [
              "integer",
              "null"
            ],
            "description": "AIS navigational status code"
          },
          "shipType": {
            "type": [
              "integer",
              "null"
            ],
            "description": "AIS ship-type code"
          },
          "destination": {
            "type": [
              "string",
              "null"
            ]
          },
          "observedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "freshness": {
            "type": "string",
            "enum": [
              "fresh",
              "stale",
              "expired"
            ]
          },
          "sidc": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reserved for the MIL-STD-2525D side-car (currently null)"
          }
        }
      },
      "GeoEntity": {
        "type": "object",
        "description": "The normalized entity shape shared by every tier (the `feed-core` contract). Sources map into it; Zenoh, Dragonfly, QuestDB, the tiler, and the frontend read it back. `properties` is arbitrary; enrichment is inlined under namespaces (`properties.vessel.*`, `properties.aircraft.*`).",
        "required": [
          "id",
          "layer",
          "source",
          "kind",
          "lon",
          "lat",
          "ts"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable per source (MMSI",
            "ICAO24": null,
            "event id)": null
          },
          "layer": {
            "type": "string",
            "description": "Catalog layer key"
          },
          "source": {
            "type": "string",
            "description": "Source-plugin id"
          },
          "kind": {
            "type": "string",
            "enum": [
              "Point",
              "LineString",
              "Polygon"
            ]
          },
          "lon": {
            "type": "number"
          },
          "lat": {
            "type": "number"
          },
          "alt_m": {
            "type": [
              "number",
              "null"
            ]
          },
          "heading_deg": {
            "type": [
              "number",
              "null"
            ],
            "description": "Orientation (ship heading / aircraft track)"
          },
          "cog_deg": {
            "type": [
              "number",
              "null"
            ],
            "description": "Course over ground"
          },
          "speed_mps": {
            "type": [
              "number",
              "null"
            ]
          },
          "ts": {
            "type": "integer",
            "format": "int64",
            "description": "Observation time",
            "epoch ms": null
          },
          "geometry": {
            "type": [
              "object",
              "null"
            ],
            "description": "Full GeoJSON geometry for non-point layers"
          },
          "properties": {
            "type": "object"
          },
          "sidc": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reserved MIL-STD-2525D passthrough"
          }
        }
      },
      "Delta": {
        "description": "The unit of change sources emit and the streaming surfaces deliver. Discriminated by `op`. `snapshot` is sent on a slow cadence (and as the first frame on subscribe) so new subscribers resync without replaying history.",
        "oneOf": [
          {
            "type": "object",
            "required": [
              "op",
              "entity"
            ],
            "properties": {
              "op": {
                "type": "string",
                "const": "upsert"
              },
              "entity": {
                "$ref": "#/components/schemas/GeoEntity"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "op",
              "layer",
              "id",
              "ts"
            ],
            "properties": {
              "op": {
                "type": "string",
                "const": "remove"
              },
              "layer": {
                "type": "string"
              },
              "id": {
                "type": "string"
              },
              "ts": {
                "type": "integer",
                "format": "int64"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "op",
              "layer",
              "entities",
              "ts"
            ],
            "properties": {
              "op": {
                "type": "string",
                "const": "snapshot"
              },
              "layer": {
                "type": "string"
              },
              "entities": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/GeoEntity"
                }
              },
              "ts": {
                "type": "integer",
                "format": "int64"
              }
            }
          }
        ]
      },
      "CatalogFeed": {
        "type": "object",
        "description": "A marketplace card in the Feed Catalog (FEEDS-wby; see feed-catalog-custom-feeds/data-model.md).",
        "properties": {
          "id": {
            "type": "string",
            "example": "opensky"
          },
          "name": {
            "type": "string",
            "example": "Aviation"
          },
          "category": {
            "type": "string",
            "example": "aviation"
          },
          "trust": {
            "type": "string",
            "enum": [
              "official",
              "verified",
              "community",
              "experimental"
            ]
          },
          "icon": {
            "type": "string",
            "example": "aircraft"
          },
          "summary": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "layer": {
            "type": "string",
            "description": "Bound catalog layer key."
          },
          "mode": {
            "type": "string",
            "enum": [
              "live",
              "static",
              "hybrid"
            ]
          },
          "installed": {
            "type": "boolean",
            "description": "Whether the calling operator has subscribed."
          },
          "stats": {
            "type": "object",
            "properties": {
              "entities": {
                "type": "integer"
              },
              "lastPoll": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          }
        }
      },
      "ReplayDescriptor": {
        "type": "object",
        "description": "Replay session descriptor (FEEDS-m0f.6). `actual_start`/`actual_end` are\nthe REAL recorded bounds from QuestDB — null when the window has no data.\n",
        "required": [
          "layer",
          "since",
          "until",
          "frame_count_estimate",
          "speed",
          "sample",
          "has_data"
        ],
        "properties": {
          "layer": {
            "type": "string",
            "enum": [
              "adsb",
              "ais"
            ]
          },
          "since": {
            "type": "integer",
            "format": "int64",
            "description": "Requested window start (epoch-ms)."
          },
          "until": {
            "type": "integer",
            "format": "int64",
            "description": "Requested window end (epoch-ms)."
          },
          "actual_start": {
            "type": [
              "string",
              "null"
            ],
            "description": "Earliest recorded timestamp in window (ISO-8601)",
            "or null.": null
          },
          "actual_end": {
            "type": [
              "string",
              "null"
            ],
            "description": "Latest recorded timestamp in window (ISO-8601)",
            "or null.": null
          },
          "frame_count_estimate": {
            "type": "integer",
            "format": "int64",
            "description": "Real row count over the window."
          },
          "speed": {
            "type": "number",
            "description": "Echoed playback speed multiplier."
          },
          "sample": {
            "type": "string",
            "description": "SAMPLE BY interval clients should request slices at."
          },
          "has_data": {
            "type": "boolean",
            "description": "False when the window has no recorded rows."
          }
        }
      },
      "ApiResponse": {
        "type": "object",
        "description": "Stable envelope wrapping every Feed Manager response.",
        "properties": {
          "data": {
            "description": "Success payload (absent on error)"
          },
          "error": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ApiError"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "ApiError": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "feature_disabled",
              "validation",
              "not_found",
              "conflict",
              "invalid_state",
              "adapter_error",
              "internal"
            ]
          },
          "message": {
            "type": "string"
          },
          "details": {
            "description": "Code-specific detail (e.g. array of FieldError for validation)"
          }
        },
        "required": [
          "code",
          "message"
        ]
      },
      "FieldError": {
        "type": "object",
        "properties": {
          "field": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "field",
          "message"
        ]
      },
      "FeedDefinition": {
        "type": "object",
        "description": "A Feed Manager control-plane feed definition.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable slug; may be omitted on create"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "layer": {
            "type": "string",
            "description": "Catalog layer key this feed produces"
          },
          "mode": {
            "type": "string",
            "enum": [
              "fixed",
              "dynamic",
              "stored",
              "cached",
              "live"
            ]
          },
          "storage": {
            "$ref": "#/components/schemas/StorageTier"
          },
          "cache": {
            "$ref": "#/components/schemas/CacheConfig"
          },
          "adapter": {
            "$ref": "#/components/schemas/AdapterRef"
          },
          "schedule": {
            "type": [
              "string",
              "null"
            ],
            "description": "cron or humantime interval; required for dynamic/cached"
          },
          "enabled": {
            "type": "boolean"
          },
          "version": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "labels": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        },
        "required": [
          "name",
          "layer",
          "mode",
          "adapter"
        ]
      },
      "StorageTier": {
        "type": "object",
        "properties": {
          "hot": {
            "type": "boolean",
            "description": "Dragonfly shared hot state"
          },
          "durable": {
            "type": "boolean",
            "description": "QuestDB history"
          },
          "memoryOnly": {
            "type": "boolean",
            "description": "in-process only"
          }
        }
      },
      "CacheConfig": {
        "type": "object",
        "properties": {
          "ttlMs": {
            "type": "integer"
          },
          "staleMs": {
            "type": "integer"
          },
          "refreshAhead": {
            "type": "boolean"
          },
          "maxEntries": {
            "type": "integer"
          }
        }
      },
      "AdapterRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "config": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Secrets by env-var NAME only"
          }
        },
        "required": [
          "id"
        ]
      },
      "AdapterDescriptor": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "poll",
              "stream",
              "static"
            ]
          },
          "layerHint": {
            "type": [
              "string",
              "null"
            ]
          },
          "configFields": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "required": {
                  "type": "boolean"
                },
                "secret": {
                  "type": "boolean"
                }
              }
            }
          }
        },
        "required": [
          "id",
          "kind"
        ]
      },
      "AuditEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "feedId": {
            "type": [
              "string",
              "null"
            ]
          },
          "action": {
            "type": "string",
            "enum": [
              "created",
              "updated",
              "deleted",
              "enabled",
              "disabled",
              "refreshed"
            ]
          },
          "actor": {
            "type": [
              "string",
              "null"
            ]
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "detail": {
            "description": "arbitrary structured context"
          }
        },
        "required": [
          "id",
          "action",
          "at"
        ]
      }
    }
  }
}