Skip to content

PMTiles

PMTiles is a single-file archive format for storing map tiles. Instead of a tile server that serves individual /{z}/{x}/{y} requests, you serve (or bundle) one .pmtiles file. MapLibre reads ranges from it directly using HTTP range requests.

About this demo

This example reads the public Protomaps demo archive (demo-bucket.protomaps.com/v4.pmtiles). That upstream file is occasionally unavailable, so the map above may load empty from time to time - it's the demo source, not the plugin. For your own apps, host your own .pmtiles (see Hosting options) for a stable URL.

Why PMTiles?

Traditional tile server PMTiles
Requires running server Static file hosting
Complex infrastructure Upload one file
Per-request cost Flat storage cost
Hard to version Just replace the file

PMTiles is ideal for: self-hosted tile data, distributing maps without a backend, and reducing operational complexity.

PMTiles is not the offline feature

A hosted .pmtiles archive is still read over the network, with HTTP range requests. For a map that works with no connection, bundle the .pmtiles file as a Flutter asset and reference it with a local path, or use Offline Regions.

How it works

flowchart TD
    APP["Your app"] --> STYLE["style.json<br/><small>(Flutter asset)</small>"]
    STYLE --> SRC["source<br/><small>type: vector<br/>url: pmtiles://...</small>"]
    SRC --> RANGE["MapLibre issues<br/>HTTP range requests"]
    RANGE --> HOST["yourhost.com/tiles.pmtiles<br/><small>or bundled as a Flutter asset</small>"]

    classDef root fill:#1f6feb,stroke:#1a5fd0,color:#fff;
    class APP root

MapLibre handles the pmtiles:// protocol internally. Beyond pointing the style at the right URL, the only extra step is the one-time protocol registration on web (see Platform support).

Step 1: Get a .pmtiles file

Options:

  • Download from protomaps.com/downloads (world extracts)
  • Convert an MBTiles file: pmtiles convert input.mbtiles output.pmtiles
  • Generate from OpenStreetMap with planetiler

For testing, use the Protomaps public demo archive:

https://demo-bucket.protomaps.com/v4.pmtiles
(Protomaps also publishes dated planet builds at https://build.protomaps.com/<YYYYMMDD>.pmtiles, but those are rotated out after a few days and are not served with CORS headers, so they can't be read from a browser - don't use one for a web demo or anything long-lived.)

Step 2: Create a style JSON

The style JSON references the PMTiles archive as a vector source. Save this as assets/pmtiles_style.json:

{
  "version": 8,
  "glyphs": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
  "sources": {
    "protomaps": {
      "type": "vector",
      "url": "pmtiles://https://demo-bucket.protomaps.com/v4.pmtiles",
      "attribution": "<a href=\"https://github.com/protomaps/basemaps\">Protomaps</a> © <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
    }
  },
  "layers": [
    {
      "id": "background",
      "type": "background",
      "paint": { "background-color": "#e8f4f8" }
    },
    {
      "id": "water",
      "type": "fill",
      "source": "protomaps",
      "source-layer": "water",
      "paint": { "fill-color": "#a8d5e5" }
    },
    {
      "id": "roads",
      "type": "line",
      "source": "protomaps",
      "source-layer": "roads",
      "paint": {
        "line-color": "#ffffff",
        "line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.5, 14, 4]
      }
    },
    {
      "id": "places",
      "type": "symbol",
      "source": "protomaps",
      "source-layer": "places",
      "layout": {
        "text-field": "{name}",
        "text-size": 12
      },
      "paint": {
        "text-color": "#333",
        "text-halo-color": "#fff",
        "text-halo-width": 1
      }
    }
  ]
}

The glyphs URL above points at the MapLibre demo font server, which exists for demos and examples. Point it at your own glyph server, or your tile provider's, before shipping. And keep the attribution string in any style you ship: OpenStreetMap data is published under the ODbL, which requires crediting © OpenStreetMap contributors with a link to openstreetmap.org/copyright, and tile producers ask to be named alongside it.

The source-layer names (water, roads, places) depend on the PMTiles schema. Protomaps uses its own schema.

Step 3: Register the asset in pubspec.yaml

flutter:
  assets:
    - assets/pmtiles_style.json

Step 4: Load it in Flutter

MapLibreMap(
  styleString: 'assets/pmtiles_style.json',
  initialCameraPosition: const CameraPosition(
    target: LatLng(48.85, 2.35),
    zoom: 10,
  ),
)

That's it on Android and iOS, where MapLibre handles the pmtiles:// protocol natively. Web additionally needs the one-time registration described under Platform support.

Step 5: Add style layers programmatically (optional)

You can add more layers on top after the style loads, just like any other source:

MapLibreMap(
  styleString: 'assets/pmtiles_style.json',
  onStyleLoadedCallback: _onStyleLoaded,
)

Future<void> _onStyleLoaded() async {
  // The style already has the PMTiles source loaded as 'protomaps'
  // Add an extra highlight layer on top
  await controller.addFillLayer(
    'protomaps',
    'parks-highlight',
    const FillLayerProperties(
      fillColor: '#4CAF50',
      fillOpacity: 0.3,
    ),
    filter: ['==', ['get', 'kind'], 'park'],
  );
}

Hosting options

Option Use case
Bundled asset Offline apps, small regional extracts (<50 MB practical limit)
GitHub Releases Free hosting via CDN, good for small-medium files
Cloudflare R2 S3-compatible, free egress, ideal for large files
AWS S3 Production, large scale
Protomaps CDN Public world basemap; check the Protomaps docs for current usage terms

Platform support

PMTiles works on all platforms: Android, iOS, and Web. On Android and iOS the pmtiles:// protocol is handled by MapLibre Native itself, with no extra code. On web it has to be registered once at startup; see PMTiles on web.