Point tileserver-rs at any SpatioTemporal Asset Catalog (STAC) API and serve the underlying Cloud-Optimized GeoTIFFs (COGs) as raster tiles — no ingest, no pre-processing, no local copy.
Three modes
| Mode | Trigger | Use when |
|---|---|---|
| Static discovery | dynamic = false (default) | One-shot visualisation of the most recent asset. Ingested once on startup. |
| Dynamic per-tile discovery | dynamic = true, max_items = 1 | Always-fresh per-tile lookups; tiles come from whichever single asset intersects. |
| Multi-asset mosaic | dynamic = true, max_items >= 2 | Composite several overlapping items (cloud-free mosaic, time-stack, multi-sensor). |
Enable the feature at build time:
cargo build --release --features stac
Static discovery
Point at a STAC API collection; tileserver-rs performs a single /search on startup, merges the discovered bounding boxes, and serves the first COG asset.
[[sources]]
id = "sentinel2"
type = "stac"
path = "https://earth-search.aws.element84.com/v1"
collection = "sentinel-2-l2a"
asset_role = "visual" # or "thumbnail" / a specific asset key
dynamic = false
max_items = 25 # cap the catalog scan
Served at GET /data/sentinel2/{z}/{x}/{y}.png.
Dynamic per-tile discovery
Flip dynamic = true and every incoming tile triggers a fresh STAC /search with the tile's WGS-84 bounding box:
[[sources]]
id = "sentinel2-dynamic"
type = "stac"
path = "https://earth-search.aws.element84.com/v1"
collection = "sentinel-2-l2a"
asset_role = "visual"
dynamic = true
max_items = 1 # single best asset per tile
Tiles always reflect the current catalog state — useful for dashboards that must show the latest imagery without restarts.
Cost: one STAC API call + one COG range-read per unique tile. Pair with a global cache (see [cache] config) to amortise hot-path cost.
Multi-asset mosaic compositing
Bump max_items to fetch multiple candidate items and composite them in-memory:
[[sources]]
id = "sentinel2-mosaic"
type = "stac"
path = "https://earth-search.aws.element84.com/v1"
collection = "sentinel-2-l2a"
asset_role = "visual"
dynamic = true
max_items = 5 # top 5 candidates, composited
pixel_selection = "first" # default; see below
Semantics:
- STAC
/searchis asked for up tomax_itemsintersecting items; results come back ranked (newest first, least cloud first — however the API sorts). - All candidates render concurrently via
futures::future::join_all, so cold-cache latency ismax(per-asset)instead ofsum(per-asset). - The configured pixel-selection method reduces the stack of RGBA layers into a single tile.
- Any single failed COG is logged and skipped — one bad asset does not blank the tile.
The result is a seamless cloud-free-ish mosaic without any preprocessing step.
Pixel selection
pixel_selection chooses how the per-asset renders combine into the final tile. Names match rio-tiler's mosaic methods so configs are interchangeable with titiler.
| Value | Behaviour | Short-circuits? |
|---|---|---|
first (default) | Highest-priority valid pixel wins; lower-priority assets fill in where the canvas is masked. | Yes — stops fetching once the tile is fully opaque. |
highest | Per-pixel maximum across all valid inputs. Useful for snow/water glare. | No |
lowest | Per-pixel minimum. Useful for deep-shadow emphasis. | No |
mean | Per-pixel arithmetic mean over valid inputs. | No |
median | Per-pixel exact median — more robust to outliers than mean. | No |
stdev | Per-pixel standard deviation — useful for change detection. | No |
count | Encode the per-pixel count of valid contributions (debug/QA). | No |
lowest_cloud_cover | Sort candidates by eo:cloud_cover ascending before feeding, then run the first reduction. Clearest-sky asset wins on top. | Yes |
Assets that lack eo:cloud_cover metadata sort to the end of the priority list and are treated as worst-case (∞ cloud cover), so any explicitly-clear asset always beats an unlabelled one.
Resampling
resampling controls how source COGs are rescaled to the 256×256 Web Mercator output grid. Defaults to bilinear; matters most when a band's native resolution differs from the served tile size (e.g. Sentinel-2 10 m → 256×256 at zoom 13 needs downsampling).
[[sources]]
id = "sentinel2"
type = "stac"
path = "https://earth-search.aws.element84.com/v1"
collection = "sentinel-2-l2a"
resampling = "cubic" # sharper than bilinear for continuous imagery
| Value | Source pixels | When to use |
|---|---|---|
nearest | 1 | Classified/categorical rasters, colourmap-indexed bands. |
bilinear (default) | 2×2 | Continuous imagery; balanced quality/speed. |
cubic | 4×4 | Sharper downsampling, moderate CPU cost. |
cubicspline | 4×4 smoothed | Softer edges; good for elevation. |
lanczos | 6×6 windowed sinc | Best quality, highest CPU. |
average | block | Heavy downsampling; anti-aliases. |
mode | most-frequent | Categorical data at low zooms. |
Sentinel-2 cross-resolution mosaics (10 m RGB + 20 m SWIR) go through the same resampler; GDAL handles the per-band rescaling automatically.
Band math
Append ?expression=<formula> to any raster tile URL to evaluate a band-math expression pixel-by-pixel before the tile is encoded. Band references use b1, b2, ... (1-based) and map to the source's native band order.
GET /data/sentinel2/{z}/{x}/{y}.png?expression=(b2-b1)/(b2+b1)
The built-in parser supports arithmetic (+ - * / ^), common functions (sin, cos, log, exp, sqrt, etc.) and parentheses — anything rio-tiler / titiler would accept via numexpr. Missing or out-of-range band references (e.g. b99 on a 3-band raster) return the original tile unchanged with a WARN-level log, so a broken expression never blanks the map.
Example indices
| Name | Expression | Notes |
|---|---|---|
| NDVI | (b2-b1)/(b2+b1) | red=b1, NIR=b2 |
| NDWI | (b1-b2)/(b1+b2) | green=b1, NIR=b2 |
| SAVI (L=0.5) | ((b2-b1)/(b2+b1+0.5))*1.5 | soil-adjusted |
| Simple ratio | b2/b1 | quick contrast boost |
Output is always a single-band greyscale PNG; pair with a colourmap-indexed MapLibre raster layer if you want false-colour visualisation.
Element84 Earth Search quick example
A fully working config for global Sentinel-2 L2A imagery via Element84's public API:
[[sources]]
id = "sentinel2"
type = "stac"
path = "https://earth-search.aws.element84.com/v1"
collection = "sentinel-2-l2a"
asset_role = "visual"
dynamic = true
max_items = 5
[[styles]]
id = "sentinel2"
path = "./data/styles/sentinel2/style.json"
name = "Sentinel-2 L2A"
Style JSON references the raster source:
{
"version": 8,
"sources": {
"sentinel2": { "type": "raster", "url": "/data/sentinel2.json", "tileSize": 256 }
},
"layers": [
{ "id": "bg", "type": "background", "paint": { "background-color": "#000" } },
{ "id": "sentinel2", "type": "raster", "source": "sentinel2" }
]
}
Supported STAC catalogs
Any STAC API that implements the /search endpoint (STAC API v1.0+) with a POST body accepting collections, bbox, and limit:
- Element84 Earth Search —
https://earth-search.aws.element84.com/v1 - Microsoft Planetary Computer —
https://planetarycomputer.microsoft.com/api/stac/v1 - USGS Landsat STAC —
https://landsatlook.usgs.gov/stac-server - NASA CMR STAC —
https://cmr.earthdata.nasa.gov/stac/… - Self-hosted stac-fastapi / stac-server
Asset selection
tileserver-rs picks one COG asset per item using this priority order:
- Asset with a
rolesarray containing the configuredasset_rolevalue. - Asset keyed by the configured
asset_rolestring. - Asset whose
typeisimage/tiff; application=geotiff; profile=cloud-optimizedor starts withimage/tiff.
3D bounding box handling
STAC items may carry 6-element bounding boxes [west, south, min_elev, east, north, max_elev]. tileserver-rs correctly extracts the 2D footprint ([0, 1, 3, 4]) so elevation metadata never displaces tiles.
Specifications
| Spec | Link |
|---|---|
| STAC Core | stacspec.org |
| STAC API | github.com/radiantearth/stac-api-spec |
| Cloud-Optimized GeoTIFF | cogeo.org |