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:
(Protomaps also publishes dated planet builds athttps://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
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.