Offline Maps — .mfdmap
Load a downloaded country (or sub-region) into MapLibre without network access.
What is .mfdmap?
An encrypted PMTiles archive. AES-256-GCM, key derived from the license token issued at purchase. PMTiles natively supports byte-range reads, so the file works directly with MapLibre's PMTiles protocol handler.
Buy + download flow
┌────────────────────┐ POST /api/downloads/purchase ┌──────┐
│ buyer (your app) │ ──────────────────────────────▶ │ MFD │
│ │ ◀── { ok, downloadId, │ │
│ │ licenseToken (15-min) } │ │
│ │ GET /api/downloads/{token} │ │
│ │ ──────────────────────────────▶ │ │
│ │ ◀── { ok, url, expiresAt } │ │
│ │ GET signed-R2-url │ R2 │
│ │ ──────────────────────────────▶ │ │
│ │ ◀── stream of country.mfdmap │ │
└──────┬─────────────┘ └──────┘
│
▼ store license token securely
decrypt with libmfdmap → MapLibreThe license token is one-shot — once you redeem the URL, the token can't be redeemed again. Store the decrypted PMTiles or the raw .mfdmap + token, not just the token.
Web (MapLibre GL JS)
import maplibregl from 'maplibre-gl';
import { Protocol } from 'pmtiles';
import { mfdProtocol } from '@mapsfordevs/offline-web';
// register both protocols (mfdProtocol handles decryption + delegates to pmtiles)
maplibregl.addProtocol('mfdmap', mfdProtocol({ token: licenseToken }));
const map = new maplibregl.Map({
container: 'map',
style: 'https://api.mapsfordevs.com/styles/standard.json',
center: [28.04, -26.20],
zoom: 11
});
// override the source URL to point at the local file
map.on('load', () => {
map.getSource('mfd').setTiles([
'mfdmap:///path/or/blob-url/za.mfdmap'
]);
});In practice, hand the .mfdmap file to a URL.createObjectURL(blob) if it lives in a Blob, or fetch from local cache (Service Worker, OPFS, IndexedDB).
iOS (Swift)
import MapLibre
import MapLibreMFDOffline // helper module shipping with @mapsfordevs/offline-ios
let licenseToken = "..."
let mfdmap = URL(fileURLWithPath: "/path/to/za.mfdmap")
let proto = MFDOfflineProtocol(token: licenseToken)
MLNNetworkConfiguration.sharedManager.protocols = [proto]
let url = URL(string: "mfdmap://\(mfdmap.path)")!
mapView.styleURL = URL(string: "https://api.mapsfordevs.com/styles/standard.json")!
mapView.style?.sources["mfd"]?.setTiles(["mfdmap://\(mfdmap.path)"])Android (Kotlin)
import org.maplibre.android.maps.MapView
import com.mapsfordevs.offline.MFDOfflineProtocol
val proto = MFDOfflineProtocol(licenseToken)
MapLibre.registerProtocol("mfdmap", proto)
map.setStyle("https://api.mapsfordevs.com/styles/standard.json") { style ->
style.getSourceAs<VectorSource>("mfd")
?.setTiles(arrayOf("mfdmap:///data/data/com.example.app/files/za.mfdmap"))
}Storage size
| Region | Typical .mfdmap size at z8-17 |
|---|---|
| Small country (Luxembourg, Lesotho) | 50–150 MB |
| Mid (South Africa, UK) | 1.5–3 GB |
| Large (Brazil, India) | 4–8 GB |
| Country aggregate (US, Russia federal districts) | 8–25 GB |
Plan storage accordingly. Most apps sell per-country, not per-continent.
Updates
Each .mfdmap is dated (_20260425). When we publish a refresh, we issue you a new download via the same purchase. Yearly licenses get all updates within the year; perpetual licenses get the version they bought (re-purchase to refresh).
Caching
You may cache the decrypted PMTiles bytes locally to avoid re-decrypting on each launch. Do not redistribute the decrypted file — it leaves the protection of the license.