Documentation menu
Self-hosting
The point of MapMap is that you can run all of it yourself: the gateway, our routing engine, an optional geocoder, and the MCP server, from one Docker Compose file. This page is the orientation and a working first boot; the full production guides (sizing, backup, upgrades, hardening, air-gapped deployment) ship inside the source checkout.
Status, honestly: the hosted gateway at https://api.mapmap.ai is live; see
conventions. Self-hosting is for when you want the same
stack on your own infrastructure, under your own keys and data boundary.
What you need
- Docker Engine 24+ with Compose v2 (
docker compose version) - ~10 GB disk and 8 GB RAM for a UK-scale first tile build (serving needs less), plus ~1.5 GB once for shared global base-map sources (ocean water polygons, Natural Earth)
- Outbound internet on first boot only (OSM extract and global base-map source downloads); air-gapped installs are supported via pre-built territory packages; a territory package bundles pre-built routing tiles and map data for a region, so no OSM download is needed
Get the code
The repository is private: source access comes with a commercial relationship
or an evaluation. Email hello@mapmap.ai; you get
the full workspace, including the distro/ Compose stack and the production
operations guides. Everything below assumes you have a checkout.
The short version
cd distro
cp .env.example .env
# Admin token: protects /admin/*. Compose refuses to start without it.
sed -i '' "s/^SN_ADMIN_TOKEN=$/SN_ADMIN_TOKEN=$(openssl rand -hex 32)/" .env # macOS
# sed -i "s/^SN_ADMIN_TOKEN=$/SN_ADMIN_TOKEN=$(openssl rand -hex 32)/" .env # Linux
Pick a territory: for a bootstrap build straight from OSM (the simplest first
run), also set in .env:
VALHALLA_TILE_URLS=https://download.geofabrik.de/europe/united-kingdom-latest.osm.pbf
Any Geofabrik extract works. The production alternative is a pre-built territory package, which skips the build entirely. Then:
docker compose up -d --build
docker compose logs -f valhalla # first boot builds tiles: ~30–60 min for the UK
While tiles build, docker compose ps shows the gateway stuck in Created;
that is expected, not broken: the gateway waits for the routing engine's
healthcheck. You are up when this succeeds:
curl -fsS http://localhost:8080/health
Create an API key
Key management lives under /admin/*, authenticated with your admin token.
All three body fields are required; there are no defaults:
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}'
Returns 201 Created:
{
"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: store it in your secret manager; only its
BLAKE3 hash is persisted. Keys, quotas and usage live in the gateway's sqlite
volume (gateway_data); back that volume up. Self-hosted deployments can
alternatively let users self-serve their own snk_ keys via POST /v1/keys
(see the API reference).
| Status | Meaning |
|---|---|
400 | Invalid name, quota or rate |
401 | Missing or wrong admin token; source .env again and check the running container's token |
First route
Prove the tiles built: a UK truck route (Dover → Birmingham), 4.0 m high,
44 t, avoiding tunnels that forbid tunnel code D loads under ADR (the
European agreement on carriage of dangerous goods by road; tunnel codes B–E
restrict which tunnels a hazmat load may use; full table in
conventions):
curl -fsS -X POST http://localhost:8080/route \
-H "Authorization: Bearer snk_your_key" \
-H "Content-Type: application/json" \
-d '{
"locations": [
{"lat": 51.1279, "lon": 1.3134},
{"lat": 52.4862, "lon": -1.8904}
],
"costing": "truck",
"adr": {
"dimensions": {
"height_m": 4.0, "width_m": 2.55, "length_m": 16.5,
"gross_weight_t": 44.0, "axle_load_t": null, "axle_count": null
},
"tunnel_code": "D", "hazmat": true
}
}'
Truncated response: a standard Valhalla trip, with the applied ADR profile echoed back:
{
"trip": {
"locations": [ { "lat": 51.1279, "lon": 1.3134 }, { "lat": 52.4862, "lon": -1.8904 } ],
"legs": [ { "maneuvers": [ "…" ], "summary": { "time": 5400.0, "length": 160.0 }, "shape": "…" } ],
"summary": { "time": 5400.0, "length": 160.0 },
"status": 0,
"status_message": "Found route between points",
"units": "kilometers"
},
"adr": {
"dimensions": { "height_m": 4.0, "width_m": 2.55, "length_m": 16.5, "gross_weight_t": 44.0 },
"tunnel_code": "D", "hazmat": true
}
}
time is seconds; length follows trip.units. A 502 here means the
routing engine is unreachable or still building; check
docker compose logs -f valhalla.
What's in the distro
| Component | Role |
|---|---|
| Gateway | The API: routing, ADR, metered keys, OpenAPI, llms.txt (port 8080) |
| Routing engine | Route computation with the bounded ADR costing extension |
| Geocoder (optional) | Forward geocoding via Photon (geocoding compose profile) |
| MCP server (optional) | Agent access over HTTP (mcp compose profile, port 8200) |
Enable the optional services with compose profiles:
docker compose --profile geocoding up -d photon # needs the photon_data volume seeded first
docker compose --profile mcp up -d mcp # agents connect to http://<host>:8200/mcp
Two ways to enable geocoding. The first-party path needs no extra
service: point SN_GEOCODE_DIR at an sn-geocode index (a single
territory's geocode layer from a signed package, or a multi-territory
index merged with snfactory geocode-merge; the hosted gateway serves a
whole-world merge this way) and the gateway serves /geocode and
/geocode/reverse in-process from the same sn-geocode engine that runs
on-device: no JVM, no separate search cluster, and identical ranking
online and off. First-party POI hits include the details object of OSM
display tags (opening hours, phone, website, …) that the Photon path
cannot provide. This is the recommended path, and the only one that works
air-gapped. The Photon path above is
the fallback: it runs the Photon release JAR on a stock JRE, and its data
volume must be seeded once with the JAR and a search-index dump (commands in
the distro quickstart inside the checkout). First-party takes precedence
when both are set. MCP configuration examples are on the
MCP page.
Engines and optional services
-
SN_ROUTING_ENGINEselects the routing engine:valhalla(default),graphhopper, orauto. Every endpoint behaves identically either way, butgraphhopperandautoneed a GraphHopper server reachable atSN_GRAPHHOPPER_URL; it is not part of the default compose stack (a commented-out service block is provided).graphhopperserves every endpoint from GraphHopper;autouses Valhalla first and falls back to GraphHopper on a Valhalla backend error. -
Route optimisation runs as a sidecar; set
SN_VROOM_URLto enablePOST /optimise. Without it, that endpoint returns503. Driver breaks, work-time limits and EU drivers'-hours generation are handled in the gateway on top of the solver; no extra service. See optimisation. -
First-party geocoding: set
SN_GEOCODE_DIRto a territory geocode index to serve/geocodeand/geocode/reversefromsn-geocodein-process (no Photon). See geocoding. -
Hosted tiles, styles, fonts and sprites are served from
SN_TILES_DIR(the<territory>.pmtilesarchives) andSN_MAP_ASSETS_DIR(glyphs and sprite sheets). -
Feedback and telemetry sinks, all off unless you set them, all wired in
distro/docker-compose.yml:Variable Service Enables SN_MAP_ISSUES_DIRboth The report_map_issueNDJSON queue; the gateway serves it atGET /admin/mcp-map-issues. Both services must agree on the path.SN_RETROS_DIRmcp Safety net for submit_integration_retro: every retro the gateway cannot take (no key, unreachable, rate-limited) is written here rather than lost. Set it even when the gateway path is configured. Nothing reads it automatically.SN_MCP_USAGE_DIRmcp One NDJSON line per MCP tool call ( {ts, tool, ok, duration_ms, error_code?}); never arguments or payloads. Unset = tracing only.SN_DEMAND_LOG_DIRgateway Coarsened demand tap: one line per metered geospatial request, timestamps floored to 5 minutes and coordinates truncated to ~5 km geohash cells. Never key ids or raw coordinates. SN_ACTIVATION_EMAILgateway Default off, and it sends real email. On a key's first successful metered call, sends one note through the configured mailer ( SN_RESEND_API_KEY/SN_MAIL_WEBHOOK_URL). Leave off unless you are the operator sending it; theactivation.first_successtracing event fires either way.Retros submitted against your deployment land in your own
integration_retrostable and are readable atGET /admin/retros; they are never forwarded to MapMap. That table is only reached by the gateway POST, so a deployment withSTUDIO_URLset but no gateway API key for the mcp service queues its retros inSN_RETROS_DIRinstead of the admin console: mint an ordinarysnk_…key and setGATEWAY_API_KEYif you want them in the console. Fuller operational notes, including the shared-path gotcha, are indeploy/hetzner/README.md§7b in the checkout. -
Per-MAU SDK metering: an MAU is a distinct
X-MapMap-Uservalue per identity per calendar month.SN_MAU_INCLUDED(default 1000) sets the included count;SN_PRICE_PER_MAU_PENCE(default 30, GBP pence) prices each new user beyond it. Billing is fail-open: an uncovered debit flags the identitymau_overdrawnfor the month and requests keep being served, never blocked. SetSN_PRICE_PER_MAU_PENCE=0to disable billing entirely (typical for self-host).
Pointing clients at your instance
Everything that talks to the hosted gateway takes a base URL, so swap in your
own origin. The web SDK accepts baseUrl in its map options; the MCP server
in the compose stack already talks to your local engine directly. Put a
reverse proxy with TLS in front of port 8080 before exposing the gateway
beyond localhost; API keys travel in the Authorization header.
Production notes
- For production tiles, prefer a signed territory package
installed into the tiles volume over the OSM bootstrap build, and clear
VALHALLA_TILE_URLSfrom.envonce you manage tiles that way, so a container recreate never triggers an accidental rebuild. - Back up the
gateway_datavolume: it holds API keys, quotas and usage. - The checkout ships a full production guide covering sizing, backup, upgrades, hardening and air-gapped deployment.
Procurement pack
For buyer's counsel and security teams, the checkout ships a due-diligence pack: an SLA template (patch SLAs, support tiers, escrow), a security overview, a pre-answered vendor-security questionnaire, the licence-compliance story (permissive-only gate, ODbL handling), a deployment checklist, and a committed CycloneDX SBOM with a licence inventory, regenerated and published with every tagged release.
Next steps
- Conventions: auth, error envelope, units, ADR tunnel codes
- Territories: pre-built territory packages and air-gapped installs
- MCP server: wire agents to your self-hosted stack
- API reference: every endpoint, including
/admin/*