Self-hosting with PMTiles + R2 byte ranges
You can serve our .mfdmap (or its decrypted PMTiles) directly from your own object storage. Costs scale with bytes shipped, not tile counts.
Why PMTiles?
A single static file. Each tile is a byte range inside it. The client (MapLibre + the pmtiles JS protocol handler) reads only the bytes it needs via HTTP Range: headers. No tile server process required.
Storage backends
Anything that supports HTTP range requests:
- Cloudflare R2 (recommended, no egress fees)
- AWS S3 + CloudFront
- Google Cloud Storage
- Self-hosted nginx with
aioenabled - GitHub release assets (small countries only, 2 GB limit)
Cloudflare R2 setup
# 1. create bucket (one-time)
wrangler r2 bucket create my-mfd-tiles
# 2. upload
wrangler r2 object put my-mfd-tiles/za.pmtiles --file=./za.pmtiles
# 3. enable public access via custom domain
# CF dashboard → R2 → bucket → Settings → Public access → Custom domainCORS
Set CORS to allow Range: requests from your map's origin:
[{
"AllowedOrigins": ["https://yourapp.com"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["Range", "If-None-Match"],
"ExposeHeaders": ["Content-Range", "Content-Length", "ETag"],
"MaxAgeSeconds": 86400
}]R2: dashboard → bucket → Settings → CORS → paste JSON. S3: bucket → Permissions → CORS.
MapLibre client
import maplibregl from 'maplibre-gl';
import { Protocol } from 'pmtiles';
const protocol = new Protocol();
maplibregl.addProtocol('pmtiles', protocol.tile);
const map = new maplibregl.Map({
container: 'map',
style: {
version: 8,
sources: {
mfd: {
type: 'vector',
url: 'pmtiles://https://tiles.yourapp.com/za.pmtiles',
attribution: '© OpenStreetMap · © MapsForDevs'
}
},
layers: [/* your layers */]
}
});Cache headers
PMTiles archives are immutable per build (za_mfd_z8-17_20260425.pmtiles). Set:
Cache-Control: public, max-age=31536000, immutableEach new build is a new filename → cache busts naturally.
Performance tips
- HTTP/2 multiplexing: a single PMTiles archive serves all tiles over one connection. Make sure your origin / CDN supports HTTP/2.
- Pre-warm: first hit on a cold edge cache is slow. After warm, range reads are <50 ms.
- Range size: each tile = ~5–500 KB. Don't tune any chunk size — let the browser do it.
Cost estimate
R2 free tier: 10 GB storage + 1 M Class A ops + 10 M Class B ops / month.
| Region archive | R2 storage cost | Egress cost (CF) | Note |
|---|---|---|---|
| 10 small countries (sum 1 GB) | $0.015/mo | $0 | Fits in free tier |
| 1 mid country (3 GB) | $0.045/mo | $0 | |
| Whole world (~250 GB) | $3.75/mo | $0 | Free egress = R2's main appeal |
License
Self-hosting is permitted on your own infrastructure for licenses you bought. Re-distributing the raw archive to other parties is not — that breaks the per-buyer license. Use the embed widget or our hosted tile API for sharing access.