# File formats

The three on-disk/wire formats an integrator meets when working offline:
the **channel index** a device polls for updates, the **installed
territory package** on the device, and the **`.drive.jsonl` replay
corpus** that drives guidance in tests and simulators. All three are
plain text/JSON over standard containers (tar, zstd), inspectable with
`ls`, `tar` and `jq`.

## Channel index (`index.json`)

The signed root of the update channel, served byte-exact as
`GET /territories` (with its detached signature at
`GET /territories/index.sig`); see
[territories](/docs/territories#fetching-territories-over-the-api). The
detached signature is base64 ed25519 over the **exact served bytes**, so
never reformat the JSON before verifying.

Top level:

| Field | Type | Meaning |
|---|---|---|
| `format_version` | integer | Index format version, currently `1`. Readers must reject versions they do not understand |
| `generated_at` | RFC 3339 string | The newest manifest `created_at` in the channel (a deterministic stand-in for wall-clock time, so republishing is byte-for-byte reproducible) |
| `territories` | array | All published territories, **sorted by id** |

Each entry of `territories[]`:

| Field | Type | Meaning |
|---|---|---|
| `id` | string | Stable territory identifier, e.g. `"uk"` (lowercase ASCII letters, digits, hyphens) |
| `display_name` | string | Human-readable name from the latest manifest |
| `latest_version` | string | The version devices should be on (greatest published version) |
| `versions` | array | All published versions, ascending |

Each entry of `versions[]`:

| Field | Type | Meaning |
|---|---|---|
| `version` | string | Package version string |
| `data_timestamp` | RFC 3339 string | OSM snapshot timestamp of the package data |
| `manifest_path` | string | Channel-relative path of the version's `manifest.json`, e.g. `territories/uk/2.0.0/manifest.json` |
| `total_bytes` | integer | Sum of all layer sizes (installed size; wire blobs are zstd-compressed and smaller) |

```json
{
  "format_version": 1,
  "generated_at": "2026-07-13T12:00:00Z",
  "territories": [
    {
      "id": "uk",
      "display_name": "United Kingdom",
      "latest_version": "2.0.0",
      "versions": [
        {
          "version": "2.0.0",
          "data_timestamp": "2026-07-01T00:00:00Z",
          "manifest_path": "territories/uk/2.0.0/manifest.json",
          "total_bytes": 123456789
        }
      ]
    }
  ]
}
```

The channel directory behind the index is nothing but static files:

```
<channel-dir>/
  index.json                     # canonical JSON, deterministic
  index.json.sig                 # base64 ed25519 over the exact bytes
  territories/<id>/<version>/
    manifest.json                # the signed package manifest, verbatim
    manifest.sig
    layers/<addr>/<name>.tar.zst # content-addressed layer blobs
```

Each layer blob is a deterministic tar + zstd archive containing the
layer file or directory under the fixed entry name `data`, so blob bytes
are a pure function of layer content; two versions that share a layer
share the blob. `<addr>` is the first **16** lowercase hex characters of
the layer's BLAKE3 hash from the signed manifest; `<name>` is the layer
path's final component plus `.tar.zst` (e.g.
`layers/91c0…16 hex…/uk.pmtiles.tar.zst`).

## Package manifest (`manifest.json`)

The signed description of one package version, served byte-exact at
`GET /territories/{id}/{version}/manifest` with its detached signature at
`…/manifest.sig`.

| Field | Type | Meaning |
|---|---|---|
| `format_version` | integer | Container format version, currently `1` |
| `territory_id` | string | Stable identifier, e.g. `"uk"` |
| `display_name` | string | Human-readable name |
| `version` | string | Package semver, bumped on every rebuild |
| `data_timestamp` | RFC 3339 string | OSM snapshot the package derives from |
| `bbox` | `[west, south, east, north]` | Bounding box in WGS84 degrees |
| `layers` | array | The map layers (below) |
| `attribution` | object | ODbL block: `odbl` (bool), `osm_attribution`, `recreation_recipe`, `notices[]` |
| `created_at` | RFC 3339 string | Build time |

Each entry of `layers[]`:

| Field | Type | Meaning |
|---|---|---|
| `kind` | string | Layer kind, kebab-case: `valhalla-tiles` (Valhalla routing tile tree), `pmtiles` (single-file render layer), `geocode-index` (offline search index), `poi-sidecar` (optional proprietary POI database). Unknown strings are carried verbatim so new kinds need no format bump |
| `path` | string | Layer file or directory, relative to the package root, forward slashes |
| `bytes` | integer | Total size (directories: sum of file sizes) |
| `blake3` | string | Lowercase hex BLAKE3 of the file, or of the deterministic directory hash tree for directory layers |

## Installed package on disk

The device-side `TerritoryStore` owns a root directory (you choose it:
e.g. `filesDir/territories` on Android, Application Support on iOS) laid
out as:

```
<root>/
  state.json          # { "active": "uk" } (written atomically)
  staging/            # temp dirs for in-flight installs (same filesystem,
                      # so promotion is an atomic rename)
  territories/<id>/   # verified, installed packages
    manifest.json     # the signed manifest, verbatim
    manifest.sig
    <layer paths…>    # exactly the layers[].path entries, e.g.
                      #   valhalla/…      (valhalla-tiles tile tree)
                      #   uk.pmtiles      (render layer)
                      #   geocode/…       (search index)
```

An installed package directory is the unpacked `.snpkg`: the manifest,
its signature and the layer files at their manifest-declared relative
paths. A `.snpkg` archive itself is the same directory packed as
deterministic tar + zstd. Treat the store's root as opaque in production
code (resolve layer paths through
`TerritoryStore.layerPath(territoryId, kind)` rather than hard-coding
them), but the layout is stable and inspectable for debugging and
backups. Installs are transactional: packages are staged inside the root,
verified end to end (ed25519 over the manifest bytes, then per-layer
BLAKE3), and only then atomically renamed into `territories/`; a package
that fails verification never becomes visible.

## `.drive.jsonl` replay corpus

The QA corpus format produced by the `sn-replay` harness and consumed by
`ReplayLocationProvider` on Android and iOS (see the
[Android API reference](/docs/android#replaylocationprovider)): a
JSON-lines file where **line 1 is a header** and every following
non-blank line is **one GPS fix**. It replays bit-identically on the JVM,
on device and in the Rust harness: record once, assert everywhere.

Header line:

| Field | Type | Required | Meaning |
|---|---|---|---|
| `format_version` | integer | yes | Currently `1`; readers reject anything newer |
| `name` | string | yes (non-empty) | Short machine-friendly corpus name |
| `description` | string | yes | Human-readable drive scenario |
| `route_fixture` | string | yes | Relative path of the route fixture the drive replays against |
| `seed` | integer | yes | PRNG seed; with `generator` makes the corpus byte-for-byte reproducible |
| `generator` | object | optional | The synthesis parameters that produced the drive (speed model, GPS noise, dropouts, …) |

Fix lines:

| Field | Type | Required | Meaning |
|---|---|---|---|
| `t_ms` | integer | yes | Milliseconds since drive start; **strictly increasing** |
| `lat` | number | yes | Latitude, `[-90, 90]` |
| `lon` | number | yes | Longitude, `[-180, 180]` |
| `speed_mps` | number | optional | Speed over ground, ≥ 0 |
| `bearing_deg` | number | optional | Course over ground, `[0, 360)` clockwise from true north |
| `horizontal_accuracy_m` | number | optional | Estimated 1-sigma horizontal accuracy, > 0 |

Validation (enforced identically by the Rust, Kotlin and Swift parsers):
supported `format_version`, non-empty `name`, at least one fix, strictly
increasing `t_ms`, finite in-range coordinates and channels.

Sample: the opening of the committed `urban-3step-clean` fixture
(`crates/sn-replay/fixtures/drives/urban-3step-clean.drive.jsonl`; the
`fixtures/drives/` directory holds seven ready-made corpora covering
motorway, urban, roundabout, tunnel-dropout and U-turn scenarios):

```json
{"format_version":1,"name":"urban-3step-clean","description":"Clean 1 Hz drive along the urban 3-step route.","route_fixture":"routes/urban-3step.route.json","seed":1001,"generator":{"preset":"clean","speed":{"kind":"constant","mps":12.0},"gps_noise_sigma_m":0.0,"bearing_noise_sigma_deg":0.0,"speed_noise_sigma_mps":0.0,"accuracy_base_m":3.0,"accuracy_jitter_m":0.0,"reacquisition_boost":1.0,"dropouts":[],"tail_fixes":3}}
{"t_ms":0,"lat":52.2,"lon":0.13,"speed_mps":12.0,"bearing_deg":90.0,"horizontal_accuracy_m":3.0}
{"t_ms":1000,"lat":52.2,"lon":0.1301761,"speed_mps":12.0,"bearing_deg":90.0,"horizontal_accuracy_m":3.0}
{"t_ms":2000,"lat":52.2,"lon":0.1303522,"speed_mps":12.0,"bearing_deg":90.0,"horizontal_accuracy_m":3.0}
```

Synthesising your own corpus from route geometry is legitimate: keep
`t_ms` strictly increasing and channels in range and any conforming file
replays. A `generator` block is optional provenance, not required for
playback.

## Next steps

- [Territories](/docs/territories): the trust model and endpoints these formats travel over
- [Android API reference](/docs/android): `TerritoryStore`, `DriveCorpus`, `ReplayLocationProvider`
- [SDKs](/docs/sdks): install coordinates and the verifying-key trust anchor
