How Terraforge serves 440,000 geotechnical investigation points across the Netherlands from a single file on Cloudflare R2 — no Postgres, no Martin, no Tegola, nothing running.
// the shape of the problem
Web maps work in tiles. The world is sliced into a pyramid: zoom level z, column x, row y. When the user pans or zooms, the browser asks for the dozen or so tiles in view and stitches them together. At zoom 0 the entire planet is one tile; at zoom 14 the planet is 268 million tiles.
The traditional way to serve those tiles is a tile server — something like Martin, Tegola, or Mapnik — that talks to a PostGIS database, runs a spatial query for each requestedz/x/y, encodes a vector tile, and ships it back. Works great, takes a real server, costs real money, and is one more moving part you can break.
// the insight
Terraforge's data is read-only — the BRO open-data registry of 440k points doesn't change between page loads. So why re-compute a tile every time someone pans the map? Generate every tile once, save the bytes, and let the CDN do what it's good at.
The naive version of this is a folder of millions of small files, one per z/x/y, sitting in a bucket. That works, but millions of small files are slow to upload, expensive to list, and painful to update. The version that actually works is PMTiles: one file, with an internal directory index, that the client reads using HTTP byte-range requests.
// what's in a .pmtiles file
A .pmtiles archive is laid out like this:
┌──────────────────────────┐ │ header (127 bytes) │ ← magic bytes + offsets to the bits below ├──────────────────────────┤ │ root directory │ ← which byte ranges hold which (z, x, y) tiles ├──────────────────────────┤ │ metadata JSON │ ← layer names, attribution, bounds ├──────────────────────────┤ │ leaf directories │ ← (only for very large archives — paged index) ├──────────────────────────┤ │ tile data — gzipped MVT │ ← all the actual map tiles, concatenated └──────────────────────────┘
When MapLibre wants tile z=10, x=520, y=339 it asks the PMTiles client, which has already cached the header + root directory (~50 KB). The client looks up the byte range for that tile — say bytes 47,123,401-47,128,907 — and issues a single HTTP request to R2 with a Range: header. R2 returns ~5 KB of gzipped MVT, MapLibre parses it, the tile shows up. No server-side code runs. R2 is a dumb file store. Cloudflare's CDN caches the byte range so the next user within a hundred-kilometre radius doesn't even hit R2.
// the whole architecture
┌──────────────┐ 1× upload ┌─────────────────────┐
│ build step │ ─────────────────▶ │ Cloudflare R2 │
│ (one-shot) │ bro.pmtiles │ bro.pmtiles │
│ Rust + GDAL │ (20 MB) │ + CDN in front │
└──────────────┘ └──────────┬──────────┘
│
│ HTTP Range:
│ bytes=47123401-47128907
▼
┌─────────────────────┐
│ browser │
│ ─ MapLibre GL │
│ ─ pmtiles client │
│ ─ deck.gl glow │
└─────────────────────┘tippecanoe, and writes a single ~20 MB bro.pmtiles file. Done once, re-run only when the upstream registry releases a new batch.Range). The Cloudflare CDN sits in front and serves cached byte-ranges globally. No D1, no Worker on the hot path for tiles — just bytes.pmtiles://https://…/bro.pmtiles source URL. The PMTiles client (a few hundred lines of TypeScript) translates every requested tile into a byte-range HTTP request. Deck.gl renders the additive-blend glow on top.// tradeoffs
// what you save
// when this pattern fits
Anywhere you have geographic data that doesn't change often and isn't user-specific: open-data registries, historical layers, base maps for niche regions, archaeological sites, environmental monitoring, anything you'd previously have run a tile server for. PMTiles + object storage + a CDN is the calm, cheap, boring answer. It scales further than most projects ever need.
The same shape works for AWS S3 + CloudFront, Cloudflare R2 + CDN, or any HTTP server that honours byte-range requests (which is every HTTP server made after 1997). The format is open, the tooling is open, the bill is small. The dumbest part of the stack is the part doing the most work.
See it live at terraforge.davidbroza.dev, or read the project write-up at /projects/terraforge →
Note: this article was written by an AI. The architecture is real and shipping in production at Terraforge — the prose isn't hand-written.