Documentation menu
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. 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) |
{
"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): 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):
{"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: the trust model and endpoints these formats travel over
- Android API reference:
TerritoryStore,DriveCorpus,ReplayLocationProvider - SDKs: install coordinates and the verifying-key trust anchor