Skip to content

Data Export Contracts

The HTTP surfaces other repos and external tools consume to pull data out of WayPoint. Two distinct families live here, and they must not be conflated:

  1. Replay export — a recorded operational timeline, streamed as NDJSON.
  2. Plan export — a current geometry snapshot, rendered as GeoJSON or KML.

This is the HTTP export contract. It is separate from wire-protocol.md, which is the Zenoh mesh shape — nothing here rides Zenoh. Terms are defined in ../architecture/glossary.md.

ORK below means operational record-keeping (automated archival of the authoritative operation record). It is one of the intended consumers, not a WayPoint component.

Replay export — recorded timeline (NDJSON)

GET /api/replay/export?from=<ms>&to=<ms>

Streams an operation's full recorded replay record for a window [from, to] as a structured newline-delimited JSON bundle — one JSON object per line. WayPoint provides the record; it does not fork timelines or run simulation. That is left to external tools.

Scope and gating

  • Operation-scoped to the active operation, and role-gated to admin within that operation. A lower op role (signals / operator / observer) gets 403; a platform system_admin bypasses the role gate. Unauthenticated → 401.
  • Window clamped to the operation lifetime (startDateendDate, UTC), identical to every other /api/replay/* endpoint. A window fully outside the op's dates yields an empty bundle (header + summary only). Clamping is the sole op-scope for layers whose rows carry no operation_id (chat, voice).
  • Response: Content-Type: application/x-ndjson, Content-Disposition: attachment; …, Cache-Control: no-store.

Line structure

A meta header line, then zero-or-more record lines grouped by layer in a fixed order, then a summary footer line.

schemaVersion is a constant 1 (EXPORT_SCHEMA_VERSION = 1). Consumers should pin it. The meta window reflects the clamped window actually exported, not the raw request.

{ "type": "record", "layer": "<layer>", "record": { ... } }

layer is one of, in stream order:

layer Contents
track operator track hits — callsign, SIDC, lat/lon, alt, heading, speed (m/s; converted to knots/km-h for display per SIDC — see glossary track_hits), timestamp
feed external feed hits (AIS / ADS-B) — track-hit shape plus feed layer ids
chat chat messages — channel, message id, sender, content, sent-at
voice voice segment metadata only, plus an audioUrl reference (see below)
drawing drawing shapes — shape id, type, payload, timestamp, deleted-at
detection force-tracking / sightings — observed-at
target targeting board entries — ref, name, priority, affiliation, SIDC, lat/lon
target_transition target state-change rows — from/to state, created-at
plan plan-of-record versions — every published seq per plan id (plan id, scope unit, title, H-hour, tasks, published-at)
orbat_change ORBAT mutation timeline — per-unit snapshot rows (kind, unit fields, member ids, timestamp)
report_requirement report tasking records — kind, target units, cadence / due DTG, seq versions
report submitted reports — RAG, narrative, DTG, location, attachment refs (blob-plane sha256)
activity audit log — kind, payload, timestamp

State-at-T is reconstructed, not stored

target rows do not carry an explicit current state. A consumer reconstructs the state at any cursor T by folding target_transition rows with createdAtMs ≤ T (this mirrors the live replay board). Transitions are exported up to the clamped to, so any cursor inside the window is reconstructable.

The plan and orbat_change layers follow the same fold-forward pattern: the plan as HQ believed it at T is the highest seq with publishedAtMs ≤ T per plan id, and the ORBAT at T is the latest orbat_change row ≤ T per unit id (a deleted row removes the unit). Both layers deliberately include rows from before the window start — a plan published (or a unit formed) before the window is still active inside it.

The summary footer carries per-layer counts, a totalRecords, and a per-layer truncated map. truncated[layer] === true means that layer hit its internal row cap and is partial — the server also logs a warning, so truncation is never silent. Treat any true as "narrow the window and re-export".

Voice audio is by reference only

Voice record lines carry segment metadata plus:

"audioUrl": "/api/replay/voice/<segmentId>/audio"

The export never inlines audio bytes. To pull a segment's audio, a consumer issues a separate request to that URL, which is itself op-gated (the segment's window must overlap the operation's clamped lifetime) and session-authenticated. This keeps the bundle from becoming an unauthenticated bulk-audio leak — audio always flows through the same op gate.

Streaming / memory

The handler pipes the service's exportForOperation async generator. Each layer is fetched lazily, one at a time, and released before the next is read, so peak retained memory is one layer's cap — never the whole operation. A multi-day exercise therefore streams without exhausting memory.

Per-layer endpoints

Beyond the all-in-one bundle, each layer has a dedicated read endpoint under /api/replay/: hits, drawings, feed-hits, chat, detections, targets, voice, voice/:id/audio, activity, reports, report-requirements, plan-records, orbat-changes.

Intended consumers

  • ORK — archive the bundle as the operation's authoritative record (tracks, comms, drawings, detections, targeting decisions, audit).
  • External replay / simulation — third-party tools replay the exercise or seed a simulator from the bundle.

Plan export — current geometry snapshot

GET /api/plan/export?format=<geojson|kml>&scope=<published|all>

Exports the operation's current draw-plane geometry — a snapshot, with no time axis. This is distinct from the replay NDJSON timeline above: the source is the draw plane as it stands now, not a recorded sequence of events.

format Content-Type
geojson application/geo+json
kml application/vnd.google-earth.kml+xml
  • scope=published exports the control measures of the current published plan records (the shapes bound to orders — the set that reached devices); scope=all exports every current drawing, clamped to the operation's lifetime window (drawing rows carry no operation_id; the clamp is the op scope, as on the replay export).
  • The KML writer XML-escapes operator-supplied text (names, descriptions) as an injection guard.
  • The response is streamed.

Stated consumers: QGIS, Google Earth, and ATAK — standard GIS / mapping tools that ingest GeoJSON or KML directly.

Where to read the code

Concern Location
Replay NDJSON export (service + stream) web/app/domains/replay/replay_export.ts
Plan GeoJSON / KML export web/app/domains/plan/export/plan_export.ts