Skip to content

Data Export Contracts

The HTTP surface other repos and external tools consume to pull recorded data out of WayPoint: replay export, an operational timeline streamed as NDJSON.

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 (startDate → endDate, 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

Membership activity provenance

PETRA 1.0 does not reconstruct membership history from a Zenoh invite/membership recorder. That legacy recorder, its startup subscription, and its compatibility catch-up path are retired. The sole authoritative history inputs are the verified connected Directory mutation receipt and Directory's audit-ledger event for each invitation, revocation, member upsert/removal, ownership transfer, channel activation, and retirement.

Web persists those inputs into the activity layer after verifying the Directory receipt and matching its request/result digests. The replay/AAR view retains the mutation kind, exact aggregate, committed version/time, result commitment, actor principal, and distinct forwarding Web machine where present. It does not store an actor private key, capability, opaque-addressing salt, or treat the activity row as reusable authority. A failed, conflicting, or connectivity-required request may be represented as an audit attempt but must not appear as a committed membership transition.

Removing or tombstoning a channel removes its current data-plane grants by complete-set omission; it does not erase earlier activity or already verified chat, voice, and position archive rows. Membership changes therefore remain visible in the operational record even though Web no longer subscribes to invite or membership transport streams.

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.

Where to read the code

Concern Location
Replay NDJSON export (service + stream) web/app/domains/replay/replay_export.ts