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:
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.
| Layer | Archive path | source-layer |
|---|---|---|
| Basemap | basemap/protomaps_planet.pmtiles | (Protomaps schema) |
| Parcels | parcels/parcels_{state}_statewide.pmtiles | parcels |
| FEMA flood | enrichment/fema/fema_flood_{state}.pmtiles | flood_zones |
| Terrain | terrain/… | (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.
// 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.