Permissive licences only, enforced in CI, SBOM per release

# Permissive licences only, enforced in CI, SBOM per release

Every MapMap distributable depends only on permissive-licensed third-party code, and that is a build gate rather than a promise. `cargo deny check licenses bans sources` runs in CI on every push to main and every pull request, against the policy in `deny.toml`, so GPL, LGPL, AGPL, SSPL, EUPL, CDDL or blanket MPL anywhere in the dependency graph fails the build before it ships. Every tagged release publishes a CycloneDX 1.5 SBOM (`sbom.cdx.json`, 492 components at the time of writing) alongside a licence inventory with full texts, so your legal team checks the graph instead of trusting us. There is one scoped exception, nine Mozilla UniFFI crates under MPL-2.0, and it is written down rather than buried.

## Why does a copyleft dependency in a navigation stack matter?

Because you find it at the worst possible moment. A copyleft licence buried three levels down a dependency tree does nothing at all until someone reads the tree, and the people who read the tree are technical diligence reviewers and defence or public-sector procurement counsel. That is the point at which you have three weeks to replace something load-bearing, and no good options.

Distribution is our business model, which is what makes this sharper for us than for a pure SaaS. We ship server binaries, Android and iOS SDKs, a WebAssembly package, Docker images and signed territory packages onto hardware we do not own and cannot inspect. A licence whose obligations trigger on distribution is not a footnote in that model. It is a shipped defect, and that is the wording in the first comment of `deny.toml`.

Navigation is also a bad neighbourhood for this specific problem. The default geocoder in an OpenStreetMap stack is Nominatim, and Nominatim is GPLv2. It is the obvious choice, it is well documented, and reaching for it on day one puts GPL in the data path of a product you intend to sell as a self-hosted distributable.

## The gate is a CI job, not a policy document

The policy lives in `deny.toml` and is enforced by the `licence-gate` job in `.github/workflows/ci.yml`, which runs on every push to main and every pull request. These are the rules a build has to satisfy:

| Rule | Setting | Effect |
|---|---|---|
| Allowed licences | MIT, Apache-2.0 (and with LLVM exception), BSD-2-Clause, BSD-3-Clause, ISC, Zlib, BSL-1.0, Unicode-3.0, Unicode-DFS-2016, CC0-1.0, CDLA-Permissive-2.0, OpenSSL, MIT-0, 0BSD | Anything else fails the build |
| Copyleft | GPL, LGPL, AGPL, SSPL, EUPL, CDDL, blanket MPL | Denied, loudly |
| `wildcards` | `deny`, with `allow-wildcard-paths = true` | No floating version ranges on third-party crates. Internal workspace path dependencies are exempt, because they carry no version by design |
| `unknown-registry` / `unknown-git` | `deny`, with `allow-git = []` | Every dependency comes from crates.io. No git branches |
| `yanked` | `deny` | A yanked crate fails the build |
| Banned crate | `openssl` | rustls only, on the reason "keeps the SBOM story clean" |

Adding a licence to that allow list requires a documented human sign-off recorded in the commit message, because the whole value of the gate is that it cannot be widened quietly by someone in a hurry.

## What permissive-only ruled out, and what it cost us

It ruled out Nominatim, OpenSSL and git dependencies, and the geocoder was by far the expensive one.

- **Nominatim (GPLv2) is banned from the codebase and its data pipelines.** So we wrote a geocoder. On-device and first-party geocoding is `sn-geocode` over tantivy (MIT); hosted geocoding is not enabled yet, and operators who prefer a proxy can run Photon (Apache-2.0) behind `SN_PHOTON_URL`, and self-hosters can point `SN_GEOCODE_DIR` at a territory geocode index and serve `/geocode` in-process with no JVM and no separate search cluster. That path is the only one that works air-gapped, so the licence constraint and the deployment constraint happened to push the same way. It still cost us a search engine we would otherwise have installed in an afternoon.
- **`openssl` is an explicit ban, not just an absence.** reqwest and sqlx are configured with `default-features = false` and rustls, which means the occasional dependency that assumes system OpenSSL has to be worked around rather than accepted.
- **No git dependencies at all.** `unknown-git = "deny"` with an empty `allow-git` means you cannot pull a patched crate off a branch to unblock yourself, which is the normal Rust escape hatch. Ferrostar is a pinned crates.io release (BSD-3-Clause). Our Valhalla changes live as an upstream-styled patch series under `fork/`, MIT like upstream, written to be sent upstream rather than carried privately.
- **Android's JNA is dual-licensed Apache-2.0 OR LGPL-2.1.** We elect Apache-2.0, and that election is recorded in `docs/LINEAGE.md` rather than left implicit.

## What is in the SBOM, and how do you check it yourself?

It is a CycloneDX 1.5 document with one entry per crate, carrying version, purl, SHA-256 hash and licence expression. `scripts/generate-sbom.sh` produces it with pinned tooling (cargo-cyclonedx 0.5.9, cargo-about 0.9.1), and the release workflow attaches `sbom.cdx.json`, `THIRD-PARTY-LICENSES.md` and `SHA256SUMS` to every `v*` tag.

A real component entry:

```json
{
"type": "library",
"bom-ref": "registry+https://github.com/rust-lang/crates.io-index#android_system_properties@0.1.5",
"name": "android_system_properties",
"version": "0.1.5",
"scope": "required",
"hashes": [
{ "alg": "SHA-256", "content": "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" }
],
"licenses": [ { "expression": "MIT OR Apache-2.0" } ],
"purl": "pkg:cargo/android_system_properties@0.1.5"
}
```

Every licence expression in the graph, counted:

```sh
jq -r '.components[] | [.licenses[]? | .expression // .license.id // .license.name]
| join(" ")' sbom.cdx.json | sort | uniq -c | sort -rn | head
```

```
269 MIT OR Apache-2.0
96 MIT
29 Apache-2.0 OR MIT
18 Unicode-3.0
10 Unknown
9 MPL-2.0
8 Unlicense OR MIT
7 Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
7 Apache-2.0
4 BSD-3-Clause
```

Two lines there deserve explanation rather than a shrug. The 10 `Unknown` entries are our own `sn-*` crates, which are proprietary and carry no SPDX identifier; `deny.toml` sets `private = { ignore = true }` because the gate exists for third-party code, not for us. The nine MPL-2.0 entries are the UniFFI family, covered below.

The SBOM records full expressions rather than the option we take, so a grep for "GPL" finds strings that look alarming and are not. `r-efi` reports `MIT OR Apache-2.0 OR LGPL-2.1-or-later` and `htmlescape` reports `Apache-2.0 OR MIT OR MPL-2.0`. In both cases the crate offers a permissive option and we take it. We would rather publish the full expression and explain it than emit a tidier document.

## The one exception: UniFFI, MPL-2.0, scoped per crate

Nine Mozilla crates (`uniffi`, `uniffi_core`, `uniffi_bindgen`, `uniffi_build`, `uniffi_macros`, `uniffi_internal_macros`, `uniffi_meta`, `uniffi_pipeline`, `uniffi_udl`) are allowed under MPL-2.0 by name in `deny.toml`, and nothing else copyleft is.

The reason is that Ferrostar's Kotlin and Swift bindings are UniFFI-generated, so the device SDK cannot avoid it. MPL-2.0 is file-scoped weak copyleft: linking unmodified MPL files into a proprietary binary imposes no obligation on first-party code, and the one obligation that does apply, source availability of those files, is satisfied because we ship them unmodified and their source is public upstream. Two things stop the exception widening on its own. It is granted per crate rather than as a blanket MPL allowance, and `generate-sbom.sh` fails the run if MPL-2.0 appears as an obligation on any non-UniFFI crate. The WASM package `@mapmap/core` carries no MPL at all: `sn-nav-web` consumes `sn-nav-core` with `default-features = false`, which drops the UniFFI surface entirely.

## What the gate does not cover

The gate reads Rust crate licences and nothing else, so three things sit outside it: OpenStreetMap's ODbL data terms, the C++ and Java components, and non-code assets like the tile schema and the label font. You should hear that from us rather than find it.

1. **ODbL is a data licence, not a code licence, and cargo-deny cannot see it.** OpenStreetMap data is © OpenStreetMap contributors under ODbL 1.0. Our routing tiles, render tiles and geocode indices are Derivative Databases, handled under ODbL §4.6 by publishing an "algorithm plus alterations" recreation recipe, and proprietary POI data stays outside share-alike by living in sidecar layers under the OSMF Collective Database Guideline. None of that removes your attribution obligation: you display "© OpenStreetMap contributors" on every rendered map and any surface republishing response data. The SDK does it by default and white-label configuration cannot remove it.
2. **Cargo tooling only sees the Rust graph.** Valhalla (MIT), MapLibre Native (BSD-2-Clause), Planetiler (Apache-2.0, build-side only and never distributed to devices), PMTiles (BSD-3-Clause), Photon (Apache-2.0) and GraphHopper (Apache-2.0) are C++ and Java, tracked by hand in `THIRD-PARTY-NOTICES.md` and `docs/LINEAGE.md`. A hand-maintained table is weaker than a build gate. It is the honest state of it.
3. **Non-code assets carry attribution obligations of their own.** The vector tile schema and packaged styles use OpenMapTiles (CC-BY-4.0), and "© OpenMapTiles" is stamped on every compiled style and cannot be removed. Labels use Noto Sans (OFL-1.1).

## What actually transfers to you

Three obligations, and no more: display OSM attribution on rendered maps and republished response data, preserve the third-party notices that ship inside our artefacts if you redistribute them internally, and nothing else. No copyleft source-disclosure obligation attaches to your integrations or your data. The exception is the standard OSM position rather than anything our stack imposes: if you modify OSM data yourself before feeding it to the map factory, you have created your own ODbL derivative and you own that recipe.

## Where to get the evidence

The full procurement pack ships inside the source checkout: an SLA template, a security overview, a pre-answered vendor-security questionnaire, the licence-compliance story with a per-claim verification table, a deployment checklist, and the committed SBOM with its licence inventory. Start at the [self-hosting guide](/docs/self-host), which covers the Docker Compose distro and what the pack contains. The repository is private while we finish the sdk-v1 release, so source access goes through hello@mapmap.ai. If you want the argument for why any of this matters before you read the evidence, [sovereign navigation](/news/sovereign-navigation) makes the case, and [territory packages](/docs/territories) covers the signed offline artefacts the same policy governs.