Skip to content
Documentation

How the platform works

Everything here is standard MapLibre and standard vector tiles. This page covers the handful of things that are specific to us.

Core concepts

A layer is distributed as a single PMTiles archive: one file containing every tile at every zoom, with an index at the front. The client reads the index, works out the byte range for the tile it wants, and asks for exactly that range over HTTP.

This is why there is no tile server in the architecture and why bandwidth is not metered — the storage layer serves byte ranges and charges nothing for egress. It is also why a layer can be handed to you as a file and used with no network at all.

Tile access

The storage bucket is private. Requests go through an edge gateway at tiles.mapsfordevelopers.com which requires a short-lived signed token, checks the requested path against an allowlist, requires a Range header, and enforces a rolling per-client byte budget.

In the browser, tokens are minted for you and refreshed before they expire. Because MapLibre's transformRequest hook does not intercept the range requests made inside the pmtiles protocol handler, a token cannot be injected that way — the archive has to be registered with a source that appends it:

tiles.jsjavascript
import { Protocol, PMTiles, FetchSource } from 'pmtiles'

class TokenSource extends FetchSource {
  constructor(url) { super(url); this.base = url }
  getKey() { return this.base }
  async getBytes(offset, length, signal, etag) {
    const token = await getToken()          // your token fetch
    this.url = `${this.base}?t=${token}`
    return super.getBytes(offset, length, signal, etag)
  }
}

const protocol = new Protocol()
protocol.add(new PMTiles(new TokenSource(ARCHIVE_URL)))

Server-side and native clients use a long-lived API key on the REST endpoints instead.

Layer and source-layer names

Archive paths follow a predictable shape. The source-layer is the name inside the tile, and getting it wrong is the single most common reason a layer renders nothing at all.

LayerArchive pathsource-layer
Basemapbasemap/protomaps_planet.pmtiles(Protomaps schema)
Parcelsparcels/parcels_{state}_statewide.pmtilesparcels
FEMA floodenrichment/fema/fema_flood_{state}.pmtilesflood_zones
Terrainterrain/…(raster-dem)

Not every state publishes a single statewide file — a few are assembled county by county. The coverage page lists which is which, and GET /api/v1/layers returns the resolved paths programmatically.

Styling

These are ordinary Mapbox Vector Tiles, so the whole MapLibre style specification applies — data-driven paint expressions, zoom interpolation, filters and feature state all behave as documented.

style.jsjavascript
// Classify parcels by assessed value, if the county publishes it
paint: {
  'fill-color': [
    'interpolate', ['linear'], ['get', 'assessed_value'],
    0,        '#1B2430',
    250000,   '#FF6B35',
    1000000,  '#FFD08A',
  ],
  'fill-opacity': [
    'interpolate', ['linear'], ['zoom'],
    12, 0.05,
    16, 0.35,
  ],
}

Attribute names come straight from the publishing county and are not normalised across states. Inspect a feature before you depend on a field being there.

Offline use

Because an archive is a single file, a bounded region can be extracted and shipped inside an application. Point the pmtiles protocol at a local file instead of a URL and nothing else in your map code changes. Offline packages are available on paid plans.

Limits

Plans are quoted in tile requests and API calls per month; see pricing. Separately from the plan, the gateway applies a rolling per-client byte budget as an anti-scraping control. Ordinary interactive map use stays well inside it; pulling whole archives does not, and will start returning 429.

If you need bulk access to a full archive, that is a legitimate thing to want — ask us and we will arrange it properly rather than have you fight the rate limiter.