50,000 free calls a month, card-free. Get an API key →

news / sovereign-navigation · raw .md
Platform9 min readMapMap engineering
A self-contained territory of contours and roads enclosed by a rectangular boundary frame, its connection lines severed at every edge.

Sovereign navigation: run the whole stack yourself

MapMap runs as a hosted API at https://api.mapmap.ai, and the identical stack runs on your own hardware from one Docker Compose file: gateway, routing engine, an optional geocoder and the MCP server, fed by signed territory packages you publish under your own keys, air-gapped if that is what your data boundary demands. It is not free. Self-hosting is a commercial platform licence tiered by fleet size plus a per-territory data subscription, and the ops burden becomes yours. What you get for that is a navigation stack nobody can reprice under you, deprecate on you, or read your routes out of.

Every route your fleet plans is a request you hand to somebody else's server. Your depots, your delivery windows, your customer addresses, the roads your vehicles actually take at 04:00: that is operational telemetry about your business, and on a hosted-only platform it leaves your network on every call. You pay per call, on a price list the vendor owns, under terms that can change between contract cycles. There is no exit worth the name, because by then your dispatcher, your driver app and your planning tools have all hard-coded one vendor's URL, parameter names and response shape.

We are not neutral about this, and the design says so. We own the routing core rather than reselling somebody else's API: the ADR costing is our own Rust, over a graph search we patch ourselves. That is what lets the identical stack run in our cloud and in your rack. The hosted gateway is a convenience, not a moat.

What actually ships in the self-host distro

The distro is one Compose file and four services, two of which are optional. From a source checkout, the whole thing is cd distro, copy .env.example, set an admin token, and docker compose up -d --build.

ComponentRole
GatewayThe API: routing, ADR, metered keys, OpenAPI, llms.txt (port 8080)
Routing engineRoute computation with the bounded ADR costing extension
Geocoder (optional)Forward geocoding via Photon (geocoding compose profile); the first-party path runs inside the gateway instead
MCP server (optional)Agent access over HTTP (mcp compose profile, port 8200)

Three switches matter to anyone sizing this. SN_ROUTING_ENGINE selects valhalla (the default), graphhopper or auto, and every endpoint behaves identically either way, though GraphHopper needs a server at SN_GRAPHHOPPER_URL and is not in the default stack. Route optimisation runs as a sidecar: set SN_VROOM_URL or POST /optimise returns 503, deliberately. Geocoding has two paths and only one works air-gapped: point SN_GEOCODE_DIR at the geocode index inside a territory package and the gateway serves /geocode and /geocode/reverse in-process from the same sn-geocode engine that runs on device, with no JVM and no separate search cluster. Photon is the fallback, and first-party wins when both are set.

Once the routing engine's healthcheck passes, your instance is yours to key:

sh
source .env
curl -fsS -X POST http://localhost:8080/admin/keys \
  -H "Authorization: Bearer ${SN_ADMIN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"name": "fleet-ops-dev", "monthly_quota": 100000, "rate_per_min": 60}'
json
{
  "id": "…",
  "name": "fleet-ops-dev",
  "key": "snk_…",
  "created_at": "2026-07-15T09:30:00Z",
  "monthly_quota": 100000,
  "rate_per_min": 60
}

The key field is shown once, and only its BLAKE3 hash is persisted. Keys, quotas and usage live in the gateway's sqlite volume (gateway_data), which means your usage data is a file on your disk rather than a row in our database. Every deployment also serves its own /openapi.json and /llms.txt, so the contract your engineers and your agents read is the one running on your box. The full first boot, including the tile build, is on the self-hosting guide.

Can it genuinely run air-gapped?

Yes, provided you install pre-built territory packages rather than bootstrapping from an OSM extract. The bootstrap path is the easy first run and it needs outbound internet: a Geofabrik download plus roughly 1.5 GB of shared global base-map sources, then 30 to 60 minutes of tile building for a UK-scale territory. A signed package skips that entirely, which is why it is also the production path.

What makes the air gap real is that the update channel is dumb by design. It is a directory of static files, servable by any web server, inspectable with ls and tar, and mirrorable into a closed network with rsync. Verify the mirror with the same command that verifies the channel:

sh
rsync -aH --delete /srv/sn-channel/ mirror:/srv/sn-channel/
snfactory channel-verify --channel-dir /srv/sn-channel --pubkey /keys/snfactory.pub

The -H preserves hardlinks between versions so the mirror does not balloon. Devices inside the gap point their fetcher at that mirror and nothing about what they accept changes, because the trust anchor is the factory public key baked into your app, not the host that served the bytes. No server-side logic participates in the trust model, and no vendor callback is part of it. Self-hosted deployments sign with their own keys, so you decide who may publish maps to your fleet, and we are not on that list unless you put us there.

Why the packages are signed rather than merely downloaded

Every package manifest is signed with ed25519, and every layer inside it is content-addressed with a BLAKE3 hash recorded in that signed manifest. Devices never trust the transport. A hostile CDN or a compromised mirror can deny you service, but it cannot alter a single byte your device will accept.

Content addressing pays for itself twice. Because any two versions that share a layer share a blob, an unchanged multi-gigabyte routing tile tree costs no extra disk on the channel and no extra download on the device: updates are differential by construction. Devices poll the signed index on their own schedule, fetch only the changed layers, verify them against the signed manifest, and apply with an atomic swap. A crash, a dropped connection, a hash mismatch or a bad signature leaves the previous version installed and untouched. The public precedents for this shape, signed metadata over content-addressed blobs on an untrusted store, are The Update Framework and OSTree. The endpoints and device flow are on the territory packages page, and we covered the design in signed territory packages and differential OTA.

A van in a tunnel still navigates

On-device routing is the reason offline is not a degraded mode. A territory package carries routing tiles, base-map tiles and a geocode index, so a device that has installed one can plan a route, render a map and search for a destination with no network at all. In a tunnel, in a steel-framed depot, at a border where roaming quietly stops: the route still computes, because the engine and the data are both on the vehicle. ADR restrictions are enforced offline too. The channel now covers the whole world, with the live GET /territories index as the source of truth for what a deployment actually publishes.

Every distributable is permissive-licence only, and the policy is machine-enforced rather than aspirational. cargo deny check licenses bans sources runs in CI on every push to main and every pull request, against an allow list of MIT, Apache-2.0, BSD-2/3-Clause, ISC, Zlib, BSL-1.0 and similar. GPL, LGPL, AGPL, SSPL, EUPL, CDDL and blanket MPL are denied, and a disallowed licence anywhere in the dependency graph fails the build. Unknown registries, git dependencies, yanked crates and wildcards are denied too.

There is exactly one exception and we would rather you heard it from us: nine UniFFI crates (© Mozilla Foundation) are MPL-2.0, granted a per-crate exception rather than a blanket one, because Ferrostar's Kotlin and Swift bindings are UniFFI-generated and the device SDK cannot avoid them. MPL-2.0 is file-scoped weak copyleft, we ship those files unmodified, and their source is public upstream, so nothing transfers to your code. The reasoning is recorded in deny.toml and THIRD-PARTY-NOTICES.md, and the SBOM script fails the build if MPL-2.0 appears on any other crate.

Evidence ships with the release, not on request. A CycloneDX 1.5 SBOM and a human-readable licence inventory with full licence texts are regenerated with pinned tooling and published with every tagged release. The checkout also carries a procurement pack: an SLA template with CVSS-tiered patch SLAs and source escrow, a security overview, a pre-answered vendor-security questionnaire, the licence-compliance story including ODbL handling, and a deployment checklist. Your counsel can check all of it without a call with us.

What sovereignty costs

Self-hosting is a licence plus data plus support, quoted annually. Here is the shape:

ComponentBillingWhat it covers
Commercial platform licenceAnnual, quotedTiered by fleet size: up to 100 vehicles, up to 1,000 vehicles, unlimited/enterprise
Territory data subscriptionPer territory, per yearSigned, QA'd, restriction-audited territory packages with ongoing OTA updates
Support and SLATieredStandard, priority, or escrow and 24/7. Security-patch SLAs and source escrow
Professional servicesQuotedCustom territory builds, bespoke restriction overlays, integration

The licence includes the Docker distro, the signed packages and OTA channel, the CycloneDX SBOM and licence-compliance pack, and support and patch SLAs per the procurement pack. Pricing is not self-serve, because fleet size and territory count set it: email hello@mapmap.ai. The hosted API, by contrast, needs no conversation at all: 50,000 included calls a month after email verification with commercial use allowed, then 0.05p per standard call and 1p per truck or ADR call. Both doors lead to the same engine.

What we are not going to pretend

  • Self-host is not free software. It is a commercial licence plus a data subscription. If you want free, the hosted free tier is genuinely free and genuinely commercial-use. Self-hosting buys control, not a zero on the invoice.
  • You own the ops. Back up the gateway_data volume, because it holds your keys, quotas and usage. Put a TLS reverse proxy in front of port 8080 before exposing it beyond localhost, because API keys travel in the Authorization header. Budget roughly 10 GB of disk and 8 GB of RAM for a UK-scale first tile build. Nobody is paging themselves at 03:00 on your behalf unless you bought the tier that says so.
  • World coverage, but your cut is on you. The world map is now published, but a territory package cut to your exact operation is still professional services, not a config flag.
  • There is no public clone URL yet. The repository is private while we finish the sdk-v1 release. Source access is real and granted by email at hello@mapmap.ai, but you cannot evaluate the distro anonymously today, and we know that is friction.
  • The channel has no freshness guarantee by default. Devices accept any signed version, which is what makes rollback-by-republish work as an operator feature. The cost is that a stale mirror can hold devices on an older legitimate version. A signed index TTL can be layered on where that matters to you.

Who this is for, and where to start

Sovereign navigation is for anyone whose route data is not allowed to leave, or whose vendor risk is not allowed to sit outside the building: defence and public sector, logistics operators under data-residency rules, and any fleet that has been repriced mid-contract and would like that to be structurally impossible next time. If you are shipping a consumer app, use the hosted API and get on with it: introducing MapMap covers that.

If you are the other reader, start with the self-hosting guide for the boot sequence and the procurement pack, then email hello@mapmap.ai with your fleet size and the territories you need. Routing and map data derive from OpenStreetMap, so credit "© OpenStreetMap contributors" when you render or republish it.