Skip to content

ADR 004: Own map tiles from a monthly Protomaps extract in Cloudflare R2

  • Status: Accepted
  • Date: 2026-10-18
  • Deciders: Owner
  • Technical Context: apps/server maps (Leaflet), infra/oci (Cloudflare), infra/tiles

Context

The website's maps loaded raster tiles straight from tile.openstreetmap.org and tiles.openseamap.org. OpenStreetMap's tile servers run on donations and are not meant as the base map of a commercial product: no service guarantee, and they can block a site at any time. A cache in front lowers the load but doesn't change that. The beta also hid the map credit the OpenStreetMap licence requires (fixed separately).

Options weighed: a commercial tile provider (fast, paid per view, a new outside dependency); caching OpenStreetMap's tiles (cheap, still dependent); a coast-only cut of our own (10–20 GB, no inland detail); copying detailed tiles on demand from Protomaps' daily builds (cheapest storage, but depends on Protomaps keeping and allowing live reads of each build); a full-detail regional extract of our own.

Decision

  • Own vector tiles, full detail (zoom 0–15), for the sailing region (11° W – 36.5° E, 30° N – 71.5° N), cut once a month from the newest Protomaps daily build. Measured 2026-10-18: 46 GB, 19.5 million tiles.
  • Stored in Cloudflare R2 (bucket sailever-tiles), served by a Cloudflare Worker on tiles.sailever.com that reads single tiles from the archive with range requests. Cloudflare caches every answer.
  • Latitude bands: the extract is stored as three band files (30–44°, 44–52°, 52–71.5° N) plus a small manifest, because a GitHub runner has only about 35 GB of disk (the first build failed on that, 2026-10-18). Bands are made and uploaded one at a time; the Worker picks the band by the tile's centre latitude. Same total size, same cost.
  • The version is part of the tile URL (/v/<YYYYMMDD>/…), so tiles are cached 30 days in the browser (immutable) and the monthly switch needs no cache purge: tiles.json (1 hour) points to the new version, the old files are deleted two days later.
  • No backup: everything in the bucket is rebuilt by the next run. It is kept outside OCI and outside Active Storage on purpose.
  • Fallback: the website draws OpenStreetMap's raster tiles when the tile host can't answer, so an outage never leaves a blank map.
  • Seamarks stay OpenSeaMap's, fetched once per tile through the same Worker and cached 30 days.

Consequences

  • Costs: about US$0.55 a month of R2 storage (46 GB − 10 GB free); Worker, R2 reads, cache and the monthly GitHub Actions run stay within free limits at beta traffic. Budget agreed: under €2 a month.
  • The map style is Protomaps' "light" theme, drawn in the browser by protomaps-leaflet (no fonts or sprites to host).
  • A larger region or a second region is a new bounding box or new bands in .github/workflows/tiles.yml (each band must stay well under the runner's free disk); the size check (30–70 GB) guards the bill and must move with it.
  • The Worker is deployed by tofu apply in infra/oci; a change to infra/tiles/worker.js needs an apply by the owner.