Skip to content

About

Core Rust implementation of the VersaTiles toolkit for converting, validating, and serving map tiles in multiple formats.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

328 stars

Watchers

4 watching

Forks

Latest commit

 

History

4,498 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Crates.io version Crates.io downloads Code coverage CI status License

VersaTiles

VersaTiles is a Rust-based tool for processing and serving tile data efficiently. It supports multiple tile formats and offers functionalities for seamless tile handling.

Table of Contents


Installation

Linux

Install VersaTiles using the provided installation script (that downloads the correct precompiled binary):

curl -Ls "https://github.com/versatiles-org/versatiles-rs/releases/latest/download/install-unix.sh" | sudo sh

That installs the latest stable release. To install a particular one — say, a release candidate — append its tag: … | sudo sh -s v5.0.0-rc.3.

MacOS

Install VersaTiles via Homebrew:

brew tap versatiles-org/versatiles
brew trust versatiles-org/versatiles
brew install versatiles

NixOS

VersaTiles is available via nixpkgs (starting from version 24.05):

nixpkgs unstable nixpkgs stable 25.11 nixpkgs stable 25.05 nixpkgs stable 24.11 nixpkgs stable 24.05

Add this snippet to configuration.nix:

environment.systemPackages = with pkgs; [ versatiles ];

Alternatively, use it in a shell environment:

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  buildInputs = with pkgs; [ versatiles ];
}

Find more details on Nix search.

Docker

Pull the latest Docker image for easy deployment:

docker pull versatiles/versatiles

npm (Node.js)

Install the Node.js bindings for use in JavaScript/TypeScript projects:

npm install @versatiles/versatiles-rs

Prerelease Versions

Test upcoming features with prerelease tags:

# Alpha (bleeding edge)
npm install @versatiles/versatiles-rs@alpha

# Beta (feature complete, testing)
npm install @versatiles/versatiles-rs@beta

# Release Candidate (final testing)
npm install @versatiles/versatiles-rs@rc

See all available versions:

npm view @versatiles/versatiles-rs versions

Building with Cargo

Ensure you have Rust installed, then run:

cargo install versatiles

Building from Source

Clone the repository and build VersaTiles manually:

git clone https://github.com/versatiles-org/versatiles-rs.git
cd versatiles-rs
cargo build --bin versatiles --release
cp ./target/release/versatiles /usr/local/bin/

Building from Source on Debian/Ubuntu (with GDAL)

# Install system dependencies and Rust
sudo apt-get update
sudo apt-get install -y build-essential pkg-config git curl
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"

# Clone, install GDAL, and build
git clone https://github.com/versatiles-org/versatiles-rs.git
cd versatiles-rs
./scripts/install-gdal.sh
cargo build --bin versatiles --release -F gdal

# Add to PATH
sudo ln -sf "$(pwd)/target/release/versatiles" /usr/local/bin/versatiles

Verifying a Release Download

Every released tarball ships with a .sha256 beside it, which both install scripts check before unpacking. A checksum only proves the archive arrived intact, though — it is served from the same place as the archive, so it does not tell you who built it.

Each tarball also carries a signed build provenance attestation: a Sigstore keyless signature recording the commit, workflow and runner that produced it. To check one:

gh attestation verify --owner versatiles-org versatiles-linux-gnu-x86_64.tar.gz

That fails on an archive someone rebuilt or modified, even if its checksum file was replaced to match.


Quick Start

Get started with VersaTiles in 3 steps:

1. Verify Installation

versatiles --version

2. Download Sample Data

# Download a small region (Berlin, ~60MB)
versatiles convert --bbox=13.0,52.3,13.8,52.7 --bbox-border=3 https://download.versatiles.org/osm.versatiles berlin.versatiles

3. Explore Your Data

# View tile information
versatiles probe berlin.versatiles

# Serve tiles locally
versatiles serve berlin.versatiles

# Access at http://localhost:8080

Common Workflows

Convert tile formats:

versatiles convert input.mbtiles output.versatiles

Filter by zoom level:

versatiles convert --min-zoom=5 --max-zoom=12 input.versatiles output.versatiles

Extract a region:

versatiles convert --bbox=13.0,52.3,13.8,52.7 world.versatiles berlin.versatiles

Usage

Core Concepts

VersaTiles works with tile containers - files or directories containing map tiles organized by zoom level (z), column (x), and row (y).

Supported formats:

Feature .versatiles .pmtiles .mbtiles .tar directory
Read ✅ ✅ ✅ ✅ ✅
Write ✅ ✅ ✅ ✅ ✅
Local file ✅ ✅ ✅ ✅ ✅
HTTP(S) read ✅ ✅ ❌ ❌ ❌
SFTP read ✅ ✅ ❌ ❌ ❌
SFTP write ✅ ✅ ❌ ❌ ❌

✅ = supported, ❌ = not supported. Remote read over HTTP/HTTPS and SFTP read/write require the streamable single-file formats (.versatiles, .pmtiles); SFTP also requires the sftp feature. All formats store opaque tile blobs, so each works with both vector and raster tiles.

Remote access: VersaTiles can read remote .versatiles and .pmtiles files via HTTPS, and write to remote servers via SFTP (requires sftp feature):

versatiles serve https://download.versatiles.org/osm.versatiles

SFTP host keys

SFTP connections verify the server's host key against ~/.ssh/known_hosts before authenticating, following OpenSSH's accept-new policy:

  • Host already known, key matches — connect.
  • Host not known yet — record the key and connect, so a first connection is not a dead end. The new entry is appended; the rest of the file is left untouched.
  • Host known, key differs — refuse to connect. Either the server was rebuilt, or the connection is being intercepted. If the change was expected, remove the stale line from known_hosts.

VERSATILES_SFTP_KNOWN_HOSTS points at a different file, or disables verification entirely when set to off — appropriate only for a host whose key changes by design, and it gives up the protection against a man-in-the-middle, including the password sent by URL authentication.

Commands

Run versatiles to see available commands:

Usage: versatiles [OPTIONS] <COMMAND>

Commands:
  convert  Convert between different tile containers
  probe    Show information about a tile container
  reduce   Reduce a tileset to what a style actually draws
  serve    Serve tiles via HTTP
  help     Show detailed help
  mosaic   Tile and assemble image mosaics
  dev      Some unstable developer tools

convert - Convert Between Tile Formats

Convert tiles between formats, filter by region or zoom, and transform coordinates.

Basic conversion:

versatiles convert input.mbtiles output.versatiles

Advanced options:

Option Description Example
--min-zoom, --max-zoom Filter zoom levels --min-zoom=5 --max-zoom=12
--bbox Extract region (lon_min,lat_min,lon_max,lat_max) --bbox=13.0,52.3,13.8,52.7
--bbox-border Add border tiles around bbox --bbox-border=3
--compress Set compression (uncompressed, gzip, brotli, zstd) --compress=brotli
--tile-format Re-encode raster tiles (avif, jpg, png, webp), optionally with ,quality[,effort], or vector tiles (mvt, mlt) --tile-format=webp,80
--swap-xy Swap X/Y coordinates (z/x/y → z/y/x) --swap-xy
--flip-y Flip tiles vertically --flip-y
--dry-run Check the pipeline and exit, reading no tiles --dry-run
--writer-option Format-specific output option (repeatable) --writer-option=allow_unclustered=true
--force Replace a destination directory that is not empty --force

Elevation tiles. Tiles whose tile schema says they hold heights (dem/mapbox, dem/terrarium, dem/versatiles) are re-encoded without loss when --tile-format names a format and no quality: a lossy encoder changes the color bytes a little, and each byte is part of a height. webp and png can do that; avif and jpg cannot, so --tile-format=avif on such tiles is refused unless you name a quality yourself (--tile-format=avif,90) and accept the loss. Other tiles get the encoder's default quality, as before.

How the output is written. A conversion builds its output beside the destination, under a .<name>.tmp name, and moves it into place as the last step. The destination therefore only ever holds a complete container: a conversion that fails part-way leaves whatever was there before exactly as it was, and leaves nothing new behind. (A crash leaves the .tmp behind; the next run to that destination clears it.) Writing to a new path costs no extra disk — the bytes live under one name and the move is free — but overwriting an existing output holds both copies until the move completes.

Writing to a directory replaces its entire contents, so a re-run over a smaller area cannot leave tiles from a previous run mixed in with the new ones. Because that deletes whatever else is inside, an existing non-empty directory is refused unless you pass --force. Overwriting a file needs no such flag: that is atomic, and the previous output survives until the new one is complete.

If a conversion finishes writing but could not read every tile, the output is complete and readable but missing tiles. It is kept as <name>.incomplete.<ext> rather than published or deleted — the destination is untouched, and nothing downstream can mistake the partial result for the conversion that was asked for.

The output path can also be an SFTP URL (sftp://[user[:pass]@]host[:port]/path) to write directly to a remote server. This requires the sftp feature. Only formats that support streaming writes (.versatiles, .pmtiles) are supported over SFTP.

Remote writes follow the same rule: the upload goes to .<name>.tmp beside the destination and is moved into place at the end, so a failed upload never leaves a corrupt file under the name clients are fetching. Replacing an existing remote file is not a single step — SFTP's rename refuses an existing target — so the old one is moved to .old first and removed once the new one is in place. There is a brief moment where the destination does not exist; what cannot happen is losing the previous upload, which is restored if the move fails.

Real-world examples:

# Extract city region from world tiles
versatiles convert --bbox=13.0,52.3,13.8,52.7 \
  world.versatiles berlin.versatiles

# Compress tiles with maximum compression
versatiles convert --compress=brotli \
  uncompressed.tar compressed.versatiles

# Convert image format for smaller file size
versatiles convert --tile-format=webp \
  tiles.mbtiles tiles-webp.versatiles

# Fix coordinate system (TMS to XYZ)
versatiles convert --flip-y \
  tms-tiles.mbtiles xyz-tiles.versatiles

# Remote conversion with zoom filtering
versatiles convert --min-zoom=1 --max-zoom=10 \
  https://download.versatiles.org/osm.versatiles \
  local-osm-filtered.versatiles

# Write directly to a remote server via SFTP (requires sftp feature)
versatiles convert tiles.mbtiles \
  sftp://user@host/path/to/output.versatiles

probe - Inspect Tile Containers

Analyze tile containers to understand their contents and structure.

Basic usage:

versatiles probe tiles.versatiles

Depth levels:

Level Flag Scans Use Case
1 -d Container metadata Quick info (zoom range, tile format)
2 -dd All tile sizes Find actual tile coverage and biggest tiles
3 -ddd Tile contents Validate MVT 2.1 conformance; reports missing extent/version, duplicate layer names, polygon winding issues, degenerate rings

Probe options:

Option Description Example
-d, --deep Scan depth; repeat for more (-d, -dd, -ddd), as in the table above -dd
--sample <PERCENT> With -ddd, read only that share of the deepest zoom level, in contiguous square windows --sample 10

Examples:

# Quick metadata check
versatiles probe tiles.versatiles -d

# Find actual zoom range with tiles
versatiles probe tiles.versatiles -dd

# Deep inspection with MVT validation
versatiles probe tiles.versatiles -ddd

# Probe remote container
versatiles probe https://download.versatiles.org/osm.versatiles -d

When -ddd reports MVT spec issues the output includes a fix: line suggesting the correct vector_repair invocation. To apply repairs:

# fix structural issues and polygon winding in one pass
versatiles convert \
  '[,vpl](from_container filename="bad.versatiles" | vector_repair)' \
  fixed.versatiles

# also drop features whose geometry cannot be decoded
versatiles convert \
  '[,vpl](from_container filename="bad.versatiles" | vector_repair drop_offenders=true)' \
  fixed.versatiles

reduce - Strip a Tileset to What a Style Draws

A tileset carries every layer, property and feature any style might ask for. A map that ships one style needs far less than that. The VersaTiles colorful style reads name and no name_* at all, so every translation in a Shortbread tileset can go; several layers are drawn as plain geometry and need no properties whatsoever.

reduce reads a MapLibre style, works out what it actually draws, and renders a VPL pipeline that keeps only that.

Basic usage:

versatiles reduce --style colorful.json tiles.versatiles reduced.versatiles

Reduce options:

Option Description Example
--style, -s MapLibre style JSON to reduce the tileset to (required) -s colorful.json
--source ID of the style source the tileset is; required if the style draws several tilesets. Repeatable --source versatiles-shortbread
--print Print the rendered VPL pipeline and exit, touching no tiles --print

Only the style layers drawing the tileset count. A style that draws one vector tileset needs nothing more; style sources with the same url or tiles are one tileset, even under different IDs. A style that draws several, say a basemap and an overlay, is refused until --source names the one the tileset is, and so is a style that draws none of the tileset's layers: both would otherwise strip the tileset of what the map shows. Hidden style layers ("visibility": "none") count for nothing; a map that shows one at runtime needs a style with it visible for the reduction.

Seeing the pipeline before running it:

--print writes the pipeline rather than executing it, so the reduction can be reviewed, edited, or kept in a .vpl file as the source of truth:

versatiles reduce --print -s colorful.json tiles.versatiles

Because inline VPL is a first-class input, the printed pipeline pipes straight into convert:

versatiles reduce --print -s colorful.json tiles.versatiles \
  | versatiles convert "[,vpl]-" reduced.versatiles

The work is done by the vector_reduce_to_style pipeline operation, so the same reduction can be written directly in VPL without going through this subcommand:

versatiles convert \
  '[,vpl](from_container filename="tiles.versatiles" | vector_reduce_to_style style="colorful.json")' \
  reduced.versatiles

What it guarantees: the reduction may keep features the style never draws, but never drops one it does. Anything in the style that cannot be interpreted — an expression outside the filter vocabulary, say — widens to "keep" rather than being ignored. Zoom levels are clamped to the tileset's own pyramid, so a style drawing a layer from z17 against a tileset that stops at z14 keeps that layer's features in the z14 tiles rather than dropping them.

serve - HTTP Tile Server

Run a local or production tile server with advanced configuration.

Basic usage:

versatiles serve tiles.versatiles
# Access at http://localhost:8080

Server options:

Option Description Default
-i, --ip Bind IP address 0.0.0.0
-p, --port Port number; 0 picks a free one 8080
-c, --config YAML configuration file -
-s, --static Serve static content from a folder or tar (repeatable) -
--minimal-recompression Fast serving (less compression) false
--disable-api Do not serve the source list at /tiles/index.json false
--follow-symlinks Let a symlink in a static folder point outside it false
--cache-control Cache-Control header for tiles and static files 4 weeks
--auto-shutdown Shut down automatically after N milliseconds -

--cache-control defaults to public, max-age=2419200, no-transform: four weeks, right for a public tile server. Set something shorter when the tiles behind a URL change. The header is sent with everything the server answers with data: tiles, the 204 of an empty tile, tiles.json and static files. It is not sent with a 404 or an error. The list of tile sources at /tiles/index.json is the exception: it always carries the default of four weeks.

Tiles without data:

A tile inside a tile source's zoom range that holds no data (open sea, or outside the covered area) is answered by tile type:

Request Vector Raster
Tile inside the zoom range, with data 200 200
Tile inside the zoom range, without data 204 404
Zoom level outside the tile source's zoom range 404 404
Coordinates that cannot exist, unknown source 404 404
The tile cannot be read 500 500
The answer takes longer than 15 s, or overload 503 503
  • A vector tile without data gets 204 No Content and the Cache-Control of a tile. A map shows the same either way, and unlike a 404 it leaves no "Failed to load resource" line in the browser console.
  • A raster tile without data gets 404 Not Found, because MapLibre GL JS then shows the tile from a lower zoom level in its place; a 204 would be drawn as a transparent tile. A raster source with gaps at its high zoom levels keeps working that way. To fill such gaps on the server instead, see raster_overscale in versatiles help pipeline.
  • A tile that exists but cannot be read (a damaged file, a remote source that refuses the request) gets 500 Internal Server Error, without the Cache-Control of a tile, and the reason goes to the log. It is not a missing tile, and must not be kept as one by a proxy.
  • A request that is not answered within 15 seconds, or that arrives while the server has more requests than it can queue, gets 503 Service Unavailable with Retry-After: 2. That is what a remote source looks like that stopped answering, and also one that answers with a server error: the request to it is repeated until the 15 seconds are over.

Headers set under extra_response_headers in the configuration file are added to every response, the 404, 500 and 503 included. A Cache-Control set there replaces the one described here and puts it on errors too, so set the cache header with --cache-control or server.cache_control instead.

--minimal-recompression, --disable-api and --follow-symlinks can be given on their own or with true/false; left out, the configuration file decides. Put a bare switch after the tile sources or before another option — a tile source directly after it would be read as its value.

With --port 0 the server binds a free port chosen by the operating system and prints it to stdout as VERSATILES_PORT=<port> before serving, so a wrapper script can discover where it landed.

By default a symlink in a folder served with --static may not resolve outside that folder: ln -s /etc/passwd public/passwd makes GET /passwd a request for a file the operator never put there, and the path is only checked as text. Symlinks that stay inside the folder work either way — their target is being served anyway. Pass --follow-symlinks to serve a tree of links deliberately.

Tiles and static files are sent with an ETag, so a client or cache whose copy has expired can revalidate it with If-None-Match and get an empty 304 Not Modified while the data is unchanged. The tag is weak: it is derived from the stored bytes, so it changes whenever the data does. Recompressing on the fly keeps it, so one tile or file has one tag for every content encoding — except for static files stored precompressed (file.br beside file, in a folder or a tar): each stored variant is its own bytes, with its own tag.

Custom tile IDs:

Assign custom IDs to tile sources using bracket syntax:

# Bracket prefix: [id]source
versatiles serve [osm]tiles.versatiles

# Bracket suffix: source[id]
versatiles serve tiles.versatiles[osm]

Access tiles at http://localhost:8080/tiles/{id}/{z}/{x}/{y} (a file extension such as .pbf may be appended to {y}; nothing else may) and the TileJSON at http://localhost:8080/tiles/{id}/tiles.json.

Static content serving:

# Serve tar archive at root
versatiles serve -s "static.tar.br" tiles.versatiles

# Serve with custom prefix
versatiles serve -s "[/assets]static.tar.gz" tiles.versatiles
# Access: http://localhost:8080/assets/...

# Multiple static sources (first match wins)
versatiles serve \
  -s "[/styles]styles.tar.br" \
  -s "[/fonts]fonts.tar.gz" \
  tiles.versatiles

Supported static formats: .tar, .tar.gz, .tar.br, .tar.zst, directories

Remote serving:

.versatiles and .pmtiles containers can be served from https://, http:// or sftp:// URLs; .mbtiles, .tar and directories have to be local.

# Serve remote tiles directly
versatiles serve https://download.versatiles.org/osm.versatiles

# Mix local and remote sources
versatiles serve \
  [local]local.versatiles \
  [osm]https://download.versatiles.org/osm.versatiles

Configuration file:

For production deployments, use YAML configuration (see Configuration section):

versatiles serve -c production.yaml

For a full description of all configuration options:

versatiles help config

Hot-reload (Linux/macOS):

When started with -c, the server reloads its configuration without downtime on SIGHUP:

# Reload after editing production.yaml
kill -HUP $(pidof versatiles)

Tile sources are updated incrementally: an unchanged source is left alone, and a changed one is opened first and then swapped in, so it stays available throughout. If a source cannot be opened, the previous one keeps serving, the error is logged, and the next reload tries again. Static sources are handled the same way, and the new list is swapped in at once; where several static sources share a URL prefix and one of them cannot be built, none of the old ones is kept for it, because the prefix does not say which one it replaces. In-flight requests always complete against the version they started with.

Tile and static sources given on the command line stay, and so do the command line's settings: a reload applies them on top of the file again, as the start did. Of the settings, only follow_symlinks takes effect on a reload; the others need a restart. A config that names two tile sources for the same URL is refused as a whole: the error is logged and the server keeps running as it is.

Graceful shutdown:

On SIGTERM (sent by docker stop, systemd and Kubernetes) and SIGINT (Ctrl+C) the server stops accepting connections, finishes the requests it is answering, and exits. It waits up to 5 seconds for them; a second signal exits at once.

dev - Developer Tools (Unstable)

Experimental tools for tile analysis and debugging.

measure-tile-sizes - Generate a visual heatmap of tile sizes:

versatiles dev measure-tile-sizes tiles.versatiles output.png

# With options
versatiles dev measure-tile-sizes \
  --level=14 \
  --scale=4 \
  tiles.versatiles output.png

Output: PNG image where brightness = 10*log2(tile_size). Use to identify large tiles or data quality issues.

export-outline - Export tile coverage as GeoJSON:

versatiles dev export-outline tiles.versatiles coverage.geojson

# Specify zoom level (default: max zoom)
versatiles dev export-outline --level=10 tiles.versatiles coverage.geojson

Output: GeoJSON polygon showing which areas have tiles. Useful for visualizing coverage in QGIS/Mapbox.

print-tilejson - Print TileJSON metadata:

# Compact JSON
versatiles dev print-tilejson tiles.versatiles

# Pretty-printed JSON
versatiles dev print-tilejson -p tiles.versatiles

Output: Standard TileJSON 3.0.0 format with attribution, bounds, zoom levels, etc.

help - Detailed Help Topics

Get detailed help for specific topics:

# Pipeline language reference
versatiles help pipeline

# Configuration file reference
versatiles help config

# Data source syntax (bracket notation, inline VPL, JSON)
versatiles help source

# Raw markdown output (for documentation)
versatiles help pipeline --raw

VersaTiles Pipeline Language

The VersaTiles Pipeline Language (VPL) allows you to define tile-processing pipelines. Operations include merging multiple tile sources, filtering, and modifying tile content.

Example of combining multiple vector tile sources:

from_merged_vector [
   from_container filename="world.versatiles",
   from_container filename="europe.versatiles" | filter level_min=5,
   from_container filename="germany.versatiles"
]

Run a pipeline from a .vpl file or inline via the [,vpl](…) source syntax, writing the result to a local or remote target or serving it live:

# read a remote pmtiles, drop high zoom levels, write the result back to SFTP
versatiles convert \
  '[,vpl](from_container filename="https://example.org/planet.pmtiles" | filter level_max=8)' \
  sftp://user@fileserver.example.org/tiles/overview.versatiles

# merge a remote base map with a local overlay and serve it live
versatiles serve '[,vpl](from_merged_vector [
  from_container filename="https://download.versatiles.org/osm.versatiles",
  from_container filename="local-overlay.versatiles"
])'

See Data Source Syntax for inline pipelines and versatiles help source for remote/SFTP details.

More details can be found in versatiles_pipeline/README.md.

Data Source Syntax

All commands accept flexible data source expressions. You can override the auto-detected name and container type using bracket notation:

[name,type]path     # prefix notation
path[name,type]     # postfix notation

This also enables inline VPL pipelines — define a pipeline directly on the command line without creating a .vpl file:

versatiles convert \
  "[,vpl](from_container filename='input.versatiles' | filter level_max=12)" \
  output.versatiles

Run versatiles help source for the full syntax reference including JSON format.


Configuration

For production deployments, use YAML configuration files for fine-grained control over the tile server.

Basic Configuration

server:
  ip: 0.0.0.0
  port: 8080
  minimal_recompression: false # true = faster, larger responses
  disable_api: false # true = no /tiles/index.json listing of the tile sources

tiles:
  - name: osm
    src: "./tiles/osm.versatiles"
  - name: satellite
    src: "https://tiles.example.com/sat.versatiles"

static:
  - src: "./static"
    prefix: "/"

Start with config:

versatiles serve -c config.yaml

Key Features

CORS Configuration - Control cross-origin access:

cors:
  allowed_origins:
    - "https://example.org" # Exact origin
    - "https://*.dev.example.org" # Any subdomain
    - "https://example.org:*" # Any port on that host
    - "null" # Sandboxed iframes, file://
    - "/^https://.*\\.example\\.org$/" # Regex, anchored
  max_age_seconds: 86400

The first four forms are matched on the parts of an origin — scheme, host labels, port — so they cannot be extended from either end.

Any other use of * is refused at startup, and the error names a replacement. Versions before 5.0 accepted open-ended globs that matched on raw string edges: https://example.* also allowed https://example.attacker.test, *example.org allowed https://notexample.org, and a scheme-less *.example.org allowed http:// as well as https://. Use https://*.example.org for subdomains and https://example.org:* for ports. A regex must be anchored with ^...$; /example\.org/ is refused.

Caching - Set the Cache-Control of tiles, tiles.json and static files:

server:
  cache_control: "public, max-age=86400, immutable"

Custom Response Headers - Add headers to every response:

extra_response_headers:
  Timing-Allow-Origin: "*"
  X-Robots-Tag: "noindex"

These are added to every response, errors included, and replace a header of the same name the server would have set. That makes this the wrong place for Cache-Control: a 404 or a 500 would then be cached like a tile. The same goes for a CDN's own cache header (CDN-Cache-Control, Surrogate-Control) unless your CDN ignores it on errors.

Every response carries X-Content-Type-Options: nosniff by default, so a browser uses the Content-Type the server sent instead of guessing from the bytes — a file with an unknown extension is served as application/octet-stream, and a browser left to guess may decide it is HTML. Setting the header in extra_response_headers overrides the default.

Multiple Tile Sources - Serve multiple tile sets:

tiles:
  # Local file
  - name: city
    src: "./city.versatiles"

  # Remote HTTPS
  - name: osm
    src: "https://download.versatiles.org/osm.versatiles"

  # MBTiles format
  - name: elevation
    src: "./terrain.mbtiles"

  # VPL pipeline (processed on-the-fly)
  - name: processed
    src: "./pipeline.vpl"

Access tiles at: http://localhost:8080/tiles/{name}/{z}/{x}/{y}

Static Content - Serve styles, fonts, and sprites:

static:
  # Tar archive at root
  - src: "./static.tar.br"
    prefix: "/"

  # Directory at custom path
  - src: "./public"
    prefix: "/assets"

Supported formats: directories, .tar, .tar.gz, .tar.br, .tar.zst

Complete Example

server:
  ip: 0.0.0.0
  port: 8080
  minimal_recompression: false
  cache_control: "public, max-age=86400"

cors:
  allowed_origins:
    - "https://myapp.com"
    - "https://*.myapp.dev"
  max_age_seconds: 86400

tiles:
  - name: basemap
    src: "https://download.versatiles.org/osm.versatiles"
  - name: satellite
    src: "./satellite.mbtiles"

static:
  - src: "./styles.tar.br"
    prefix: "/styles"
  - src: "./fonts.tar.gz"
    prefix: "/fonts"

Full Reference

For complete configuration documentation, see:

Or run:

versatiles help config

Global Options

These accept the same values on every subcommand:

Option Description Environment variable
-v, --verbose More logging: -v debug, -vv trace (default: info) -
-q, --quiet Less logging: -q warnings, -qq errors, -qqq none -
--cache-dir Directory for temporary cache files VERSATILES_CACHE_DIR
--ssh-identity SSH identity file for SFTP authentication VERSATILES_SSH_IDENTITY

The flag wins where both are set.

Environment Variables

VersaTiles supports the following environment variables:

  • VERSATILES_CACHE_DIR - Enable disk-based tile caching. This is useful if you want to convert large tile sets with the from_gdal_raster VPL operation but have limited memory. Example: VERSATILES_CACHE_DIR=/tmp/versatiles_cache
  • VERSATILES_SSH_IDENTITY - SSH identity (private key) file used for SFTP authentication.
  • VERSATILES_MAX_TILES - Refuse a conversion whose pyramid holds more than this many tiles (default 10000000000). Guards against a source advertising a pyramid too deep to ever finish. Set to 0 to disable.
  • VERSATILES_MEMORY_LOG_SECS - Interval in seconds for logging the process's resident memory during long commands (default 60); set to 0 to disable. Reads /proc/self/status, so it is Linux-only and a no-op elsewhere. Useful when a conversion is being killed by the OOM killer, which the process cannot report itself.

Network resilience for remote reads/writes (HTTP and SFTP) over long-running transfers:

  • VERSATILES_NET_MAX_RETRIES - Retries after the first attempt for each network read/write (default 32).
  • VERSATILES_NET_RETRY_BASE_MS - Initial retry backoff in milliseconds; doubles each retry (default 1000).
  • VERSATILES_NET_RETRY_MAX_MS - Upper bound for a single retry backoff in milliseconds (default 60000). With the defaults, a single operation tolerates roughly 25–30 minutes of continuous failure before giving up, so a long unattended transfer survives a storage/CDN outage instead of aborting.

SFTP connection tuning:

  • VERSATILES_SFTP_TIMEOUT_MS - Per-operation SFTP API timeout in milliseconds (default 30000). Raise it if you see "API timeout expired" / "draining incoming flow" errors on congested links.
  • VERSATILES_SFTP_KEEPALIVE_SECS - TCP and SSH keepalive interval in seconds (default 15), keeping connections alive across idle gaps.
  • VERSATILES_SFTP_MAX_CONNECTIONS - Maximum number of pooled SFTP connections per server.
  • VERSATILES_SFTP_KNOWN_HOSTS - Known-hosts file used to verify SFTP server keys (default ~/.ssh/known_hosts). Set it to off to skip verification, which also skips the protection against a man-in-the-middle. See SFTP host keys.

Memory for tile-gathering operations:

  • VERSATILES_MAX_TILES_IN_FLIGHT - Upper bound on the number of raw source tiles held in memory at once by operations that combine sources (from_merged_vector, from_stacked, from_stacked_raster); default 2048. Peak memory ≈ this × the largest tile size, so lower it for very large tiles or many sources (e.g. 512), or raise it for more read-ahead on small tiles.
  • VERSATILES_OVERVIEW_CACHE_MB - Memory in megabytes for the block cache raster_overview and dem_overview build lower zoom levels through (default 2048). Half of it is reserved for blocks a traversal has already scheduled a consumer for, which is what lets a depth-first conversion build every tile exactly once; the rest is split evenly across zoom levels, keeping the low zooms — the ones that cost the most to rebuild — resident. Raise it if a conversion warns that the reserved half is full, which means some blocks are being built twice.
  • VERSATILES_OVERVIEW_REQUEST_TILES - Ceiling on how many source tiles one single-tile request may read while raster_overview or dem_overview builds it (default 16384); 0 removes it. The cost is counted before anything is read, so a request over the ceiling is refused at once. Building a low zoom level on demand costs up to 256 x 4^(levels below the base) source tiles, and the whole base level for zoom 0, so over a dense source a request for a very low zoom is work for a conversion rather than for someone waiting on a response. Bulk streaming — what a conversion does — is never limited.

Memory for reading containers (PMTiles / VersaTiles):

  • VERSATILES_CHUNK_MAX_BYTES - Maximum size of a single coalesced byte-range read when streaming tiles, in bytes (default 67108864 = 64 MiB). Each chunk is read as one in-memory blob.
  • VERSATILES_CHUNK_READ_MEMORY - Budget for total in-flight chunk-read bytes (default 268435456 = 256 MiB). The number of chunks read concurrently is budget / chunk_size, so peak read memory stays near this value regardless of CPU count. Lower it on memory-constrained machines, or raise it to read further ahead on fast links.
  • VERSATILES_MAX_DECOMPRESSED_BYTES - Ceiling on the result of a single decompression, in bytes (default 268435456 = 256 MiB); 0 removes it. Compressed data in a container is untrusted input, and gzip, Brotli and Zstd all expand zeroes by a factor of a thousand or more, so without a ceiling a small crafted file decides how much memory the process asks for. The ceiling is far above anything the formats produce in practice — the largest blob VersaTiles decompresses in one call is a .versatiles block index, under a megabyte — so raise it only if a legitimate input hits it.
  • VERSATILES_MAX_HTTP_BODY_BYTES - Ceiling on a whole HTTP response body read from a remote source, in bytes (default 268435456 = 256 MiB). Applies to reads that fetch an entire document — a remote .vpl, a file a pipeline names by URL, a TileJSON, a file served from a remote static folder — where nothing in the request says how much to expect. Range reads are not covered by it: they are bounded by the range they asked for, and a response carrying a different number of bytes is refused regardless of this setting.

Memory for writing .versatiles containers:

  • VERSATILES_WRITE_TILE_BUFFER - Number of compressed tiles buffered between the parallel compressor and the serial writer (default 64). Tiles are streamed straight to the output, so peak writer memory is ≈ this × the tile size — independent of how large a block is. Lower it for very large tiles on memory-constrained machines, or raise it for more write-ahead.

Example usage:

# In one line
VERSATILES_CACHE_DIR=/var/cache/versatiles versatiles serve tiles.versatiles

# or
export VERSATILES_CACHE_DIR=/tmp/versatiles_cache
versatiles serve https://download.versatiles.org/osm.versatiles

GDAL support

versatiles supports GDAL since v1.0.0. However, this is still experimental.

Building with GDAL support

If you require GDAL support:

  1. Install GDAL via your system package manager: ./scripts/install-gdal.sh (Supports Debian/Ubuntu, Alpine, and macOS via Homebrew)
  2. Build with the gdal feature: cargo build -F gdal --release

Development

VersaTiles is built with Rust and includes Node.js bindings (NAPI-RS).

Prerequisites

Required:

  • Rust 1.98+ (installation)
  • Node.js 22.12+ (for Node.js bindings)

Optional:

Setup

# Clone repository
git clone https://github.com/versatiles-org/versatiles-rs.git
cd versatiles-rs

# Build debug version
cargo build --bin versatiles

# Run tests
cargo test

# Run binary
./target/debug/versatiles --version

Development Workflow

Running All Checks

Verify code quality before committing:

./scripts/check.sh

This runs:

  • Rust: formatting (rustfmt), linting (clippy), type-checking, tests, doc tests
  • Node.js: formatting (Prettier), linting (ESLint), type-checking (TypeScript), tests (Vitest)

Quick Fixes

Rust:

# Auto-format
cargo fmt-all

# Auto-fix clippy warnings
cargo clippy --fix

# Run specific tests
cargo test raster_overscale
cargo test --package versatiles_pipeline

Node.js:

cd versatiles_node

# Auto-fix all issues
npm run fix

# Individual tasks
npm run format        # Prettier
npm run lint:fix      # ESLint auto-fix
npm run typecheck     # TypeScript check
npm test              # Vitest

Building

Debug build (fast compilation, slow execution):

cargo build --bin versatiles
# Output: ./target/debug/versatiles

Release build (slow compilation, fast execution):

cargo build --bin versatiles --release
# Output: ./target/release/versatiles

With GDAL support:

# Install GDAL (via system package manager)
./scripts/install-gdal.sh

# Build with GDAL feature
cargo build --bin versatiles --release -F gdal

Testing

# All tests
cargo test

# Specific package
cargo test --package versatiles_core
cargo test --package versatiles_pipeline

# Specific test
cargo test raster_overscale

# With output
cargo test -- --nocapture

# Ignored tests (long-running)
cargo test -- --ignored

Documentation

# Generate docs
cargo doc --no-deps --open

# Test docs
cargo test --doc

Pre-commit Hooks (Recommended)

Install Lefthook for automatic quality checks:

# macOS
brew install lefthook

# Linux
curl -fsSL https://raw.githubusercontent.com/evilmartians/lefthook/main/install.sh | sh

# Windows
scoop install lefthook

# Enable hooks
lefthook install

Hook behavior:

  • pre-commit: Fast checks (formatting, basic linting)
  • pre-push: Full checks (all tests, clippy, type-checking)

Skip hooks when needed:

# Skip pre-commit
LEFTHOOK=0 git commit -m "message"

# Skip pre-push
git push --no-verify

Node.js Bindings Development

See versatiles_node/CONTRIBUTING.md for detailed workflow.

Quick reference:

cd versatiles_node

# Install dependencies
npm install

# Build Rust bindings
npm run build

# Build debug (faster compilation)
npm run build:debug

# Run tests
npm test

# Full check before commit
npm run check

Contributing

We welcome contributions! Please:

  1. Fork and create a feature branch:

    git checkout -b feature/my-feature
  2. Make changes and test:

    ./scripts/check.sh
  3. Commit with clear messages:

    git commit -m "feat: add new feature"
  4. Push and create pull request:

    git push origin feature/my-feature

Commit message format:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation only
  • style: Formatting, no code change
  • refactor: Code restructuring
  • test: Adding tests
  • chore: Maintenance tasks

Repository Structure

Code

  • /versatiles/ - Main library and binary
  • /versatiles_container/ - Handles tile containers (*.versatiles, *.mbtiles, *.pmtiles, etc.)
  • /versatiles_core/ - Core data types and utilities
  • /versatiles_derive/ - Derive macros for the library
  • /versatiles_geometry/ - Handles geometric data (OSM, GeoJSON, vector tiles, etc.)
  • /versatiles_image/ - Manages image data (PNG, JPEG, WEBP)
  • /versatiles_pipeline/ - VersaTiles Pipeline for efficient tile processing

Dependencies of the versatiles packages:

---
config:
  layout: elk
---
flowchart TB
    versatiles --> versatiles_container
    versatiles --> versatiles_core
    versatiles --> versatiles_derive
    versatiles --> versatiles_geometry
    versatiles --> versatiles_image
    versatiles --> versatiles_pipeline
    versatiles_container --> versatiles_core
    versatiles_container --> versatiles_derive
    versatiles_container --> versatiles_geometry
    versatiles_container --> versatiles_image
    versatiles_core --> versatiles_derive
    versatiles_geometry --> versatiles_core
    versatiles_geometry --> versatiles_derive
    versatiles_image --> versatiles_core
    versatiles_image --> versatiles_derive
    versatiles_node --> versatiles
    versatiles_node --> versatiles_container
    versatiles_node --> versatiles_core
    versatiles_node --> versatiles_geometry
    versatiles_pipeline --> versatiles_container
    versatiles_pipeline --> versatiles_core
    versatiles_pipeline --> versatiles_derive
    versatiles_pipeline --> versatiles_geometry
    versatiles_pipeline --> versatiles_image
Loading

Helpers

  • /docker/ - Dockerfile for Linux builds
  • /testdata/ - Test files for validation
  • /scripts/ - Development and CI/CD automation scripts — see scripts/README.md for the full reference. Most commonly used: check.sh, build-release-with-gdal.sh.

Using as a Library

VersaTiles can be used as a command-line tool or integrated into Rust projects as a library. Check out crates.io and docs.rs for more details.


Versioning

Version numbers follow semantic versioning for what you build on: the container formats, the commands convert, probe and serve with their documented options, and the URLs of tiles and TileJSON as documented (/tiles/{name}/{z}/{x}/{y}, /tiles/{name}/tiles.json). These change in a major release only.

The pipeline language and its operations, the status codes of everything but a tile with data and the Rust API may change in a minor release. Such a change is listed under "Upgrading" in the release notes, and a removed parameter is an error that names its replacement. If you depend on the Rust crates, pin the minor version (versatiles = "~5.1"). The details are in RELEASING.md.


Additional Information

For advanced usage, guides, and detailed documentation, visit the official documentation.


For Maintainers

Creating a Release

See RELEASING.md for the complete release process.

Quick version:

# Interactive mode - select release type from menu
./scripts/release-package.sh

# Or use command-line argument
./scripts/release-package.sh patch  # or minor/major/alpha/beta/rc/dev

# Push to trigger automated release
git push origin main --follow-tags

The GitHub Actions workflow will automatically:

  • Build CLI binaries for 8 platforms (Linux gnu/musl x64/arm64, macOS x64/arm64, Windows x64/arm64)
  • Build NAPI-RS bindings for Node.js (8 platform-specific packages)
  • Publish to npmjs.com (@versatiles/versatiles-rs + 8 platform-specific packages)
  • Create GitHub release with CLI binaries
  • Trigger Docker and Homebrew updates

Contributing

VersaTiles is actively developed, and contributions are welcome! If you find bugs, need features, or want to contribute, please check the GitHub repository and submit an issue or pull request.


License

This project is licensed under the MIT License. See the LICENSE file for details.

About

Core Rust implementation of the VersaTiles toolkit for converting, validating, and serving map tiles in multiple formats.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

328 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages