Skip to content

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 aio enabled
  • GitHub release assets (small countries only, 2 GB limit)

Cloudflare R2 setup

bash
# 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 domain

CORS

Set CORS to allow Range: requests from your map's origin:

json
[{
  "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

js
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, immutable

Each 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 archiveR2 storage costEgress cost (CF)Note
10 small countries (sum 1 GB)$0.015/mo$0Fits in free tier
1 mid country (3 GB)$0.045/mo$0
Whole world (~250 GB)$3.75/mo$0Free 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.