Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
1.16.1 - 2026-09-07
Fixed
- generate the config schema identically on every OS (#3276)
1.16.0 - 2026-09-06
Aliases for fonts, sprites and tile sources
A style no longer has to spell out a combination of sources on every request.
fonts.aliases, sprites.aliases and the top-level aliases block each name a combination of sources that a client requests like a single one: a named font stack served in the listed fallback order, one sprite name covering several sprite sources, and /{alias}/{z}/{x}/{y} serving exactly what /{a},{b} serves.
aliases:
basemap: [roads, buildings]
fonts:
aliases:
Noto Sans Stack: [Noto Sans Regular, Noto Sans Arabic Regular]
sprites:
aliases:
icons: [transit, poi]
An alias may only reference real sources, never another alias, and it may share the name of a source it references, so an existing name can gain fallbacks or icons without touching a single style. Every alias is validated at startup, listed in the catalog under its own name, and its cache keys are the expanded sources, so purging a source also drops what its aliases served. See the documentation for tile, font and sprite aliases. Done in #3229, #3210 and #3220.
CQL2 filters on table sources
A table source serves every row of its table, and narrowing it down used to mean writing a function source that re-declares by hand what table discovery already knows.
A configured table now takes a filter written in CQL2 text, which Martin validates at startup and translates to SQL for the tile query, so a typo fails at load rather than on the first tile.
The filter also narrows the computed bounds, and two entries on the same table with different filters are two distinct sources instead of the duplicate they used to warn about. See the documentation. Done in #3238.
A terminal dashboard behind --tui
martin --tui turns the terminal Martin runs in into a live view of the server: the sources with their request counts and timings, the request rate, the tile requests of the last minute on a world map, and the log in a pane at the bottom, with q stopping the server.
It is meant for the moments where Prometheus and Grafana are too much setup, such as a demo, a first run against a new database, or a look at a deployment over ssh.
The flag refuses a stdout that is not a terminal, so a service or a container keeps its plain log stream.
See the documentation.
Done in #3244.
Added
- (unstable-cog) an explicitly configured remote HTTP or S3-compatible COG object is read through a new async, range-based reader over the object-store configuration PMTiles uses, so authentication, endpoint, proxy and client options carry over, credentials are redacted from logs, errors and
--save-configoutput, and malformed metadata is an error rather than a panic. This is a step of #875: such an object is loaded once at startup, and replacement detection, remote prefix listing and the matching docs are not yet implemented (#3266) - (unstable-duckdb) GeoParquet sources became usable on the datasets they are meant for. Locations accept
s3,gs,gcs,r2,az,azure,abfssandhfURLs on top ofhttp(s)and are handed to DuckDB to read: ans3://path (see Overture Maps documentation) could not be configured at all before, and DuckDB expands globs over it but not over plain HTTP, so one source can cover a dataset split across part files. Tile queries then prune row groups with the file's covering bbox, so a tile reads the parts of the file that can contain it instead of all of them. duckdb lacks this due to the interop betweenduckdb-spatial<->duckdb-parquetthis would require by default. (#3214, #3215) - (discovery) an opt-in
recursive: truenext topathsfor the mbtiles, pmtiles, cog and geojson kinds. A nested file is published under its path relative to the scanned directory with/replaced by., so2024/roads.pmtilesbecomes2024.roadsand no existing id moves. The watcher follows the same flag (#3203) - (sprites, styles)
collections, which publishes every directory inside a listed root as its own source: a sprite per directory, and every.jsoninside one as<directory>.<file>. That is a per-project layout in one line of config, without the single merged sprite a recursivepathsentry produces (#3230) - (cache) a second cache entry keyed by the negotiated encoding, so a tile a source hands over uncompressed is no longer compressed again on every request. On a warm cache against PostgreSQL sources,
get_tile_contentdropped from 107 µs to 29 µs on average and throughput rose from 103k to 114k req/s (#3211) - (postgres) an overloaded function is one source that routes on the request: a bare URL runs the queryless variant, a URL with a query string the query-taking one, and any further variant becomes its own source with a
.1suffix. Previously whichever variant the discovery query listed last won, behind a warning nobody could act on (#3243) - (postgres) functions returning gzip- or zlib-compressed tiles are detected by magic bytes and passed through to clients that accept the encoding, instead of being compressed a second time (#3236)
- (postgres) a function's key column is served as the tile ETag instead of a hash of the tile bytes, which was never stable for a PostGIS tile since PostgreSQL does not fix feature order without an
ORDER BY(#3219) - (postgres)
postgres.connection_retries(--pg-connection-retries) retries the first connection once per second, 30 times by default, so Martin no longer exits when the database is not accepting connections yet a few seconds into a container stack boot. A wrong password or database name still fails at once (#3231) - (cache) a per-connection
postgres.cacheand a per-kindcachein thembtiles,pmtiles,cog,geojson,duckdbandpassthroughblocks, layered over the global bounds. Keeping a whole discovered schema or kind out of the cache no longer means declaring every source (#3221, #3223) - (martin-cp) copying zoom by zoom and skipping the tiles below an empty tile, wherever the source promises that an empty tile only has empty tiles below it, which PostgreSQL table sources do. On the test database's
table_sourceover z0-z10, a world-bbox run went from 243 s and 1,398,101 fetched tiles to 11 s and 15,557, writing the same 14,619 tiles (#3208) - (mbtiles) a startup warning naming the missing
CREATE UNIQUE INDEXwhen a served file has no index on(zoom_level, tile_column, tile_row), which made every tile request scan the table (#3234) - (mbtiles) every hash is computed with the algorithm the file records in
hash_algorithm, and every file the tool creates records one.fnv1a,xxh64,xxh3and tippecanoe'sfnv1a-decimaljoinmd5, which stays the meaning of a file without the key (#3262) - (mbtiles)
copytakes any number of source files before the destination and copies them in order, with--on-duplicatedeciding which file wins (#3239) - (mbtiles) progress for the long commands: a bar for
unpack, a running tile count forpack, and a spinner with the elapsed time forcopy,diff,apply-patchandvalidate. Nothing is drawn when stderr is not a terminal (#3235)
Fixed
- (duckdb) cast or drop the property columns
ST_AsMVTcannot encode, instead of failing the tile (#3213) - (contour) run traced tiles through the MLT transcoder, so a contour source honours
convert_to_mlt(#3202) - (reload) apply the global cache zoom bounds to discovered files (#3222)
- (mbtiles) drop the kind-level
convert_to_hillshadeandconvert_to_contourfields, which an inherited value would also have applied to sources that cannot be shaded (#3199) - (reload) recover when a project or collection directory is removed mid-rescan, and recheck when a new directory appears (#3263, #3258, #3240)
- address misc static-analysis findings across docs, justfile, UI and tile-utils (#3259)
- (deps) update npm dependencies (#3249, #3205)
Other
- (postgres) startup and query work that is not needed is skipped: only geometry columns that can hold arcs are linearized, the catalog queries for source kinds nothing publishes are not run, and the reload poll starts one interval after init (#3224, #3225, #3226)
- (pmtiles) extract the shared object-store configuration the remote COG reader builds on (#3242)
- (config) classify source paths through a
SourceLocationtype (#3218) and enable the serde_jsonpreserve_orderthat cql2 requires (#3241) - CI and lints: clippy fixes, a lighter
gen-schemas, a shorter pipeline, a Windows-only unit test job that passes again, and some hardening and codeql cleanup (#3272, #3265, #3267, #3269, #3256) - (deps) cargo and npm dependency updates and pre-commit autoupdates (#3253, #3233, #3204, #3201)
- (docs) the cache zoom default is overridable per source, not per source type (#3198)
1.15.0 - 2026-08-31
Hillshade postprocessing
Martin can now bake a hillshade from a source serving Mapzen normal tiles, so clients get shaded relief instead of a normal map they have to shade themselves.
passthrough:
sources:
terrain:
url: https://elevation-tiles-prod.s3.amazonaws.com/normal/{z}/{x}/{y}.png
maxzoom: 14
convert_to_hillshade: auto
auto bakes with the defaults, a map of settings overrides light angle, exaggeration, contrast, banding and output format (png, webp or jxl).
It is a per-source key only, since an inherited value would also reach sources that cannot be shaded.
Baking reads a 3x3 neighborhood so tiles do not have seams at their edges, and caches the normal maps rather than the baked output.
See the documentation.
Done in #3180.
Contour postprocessing
convert_to_contour traces contour lines from a source serving Mapzen Terrarium elevation tiles and serves them as MVT.
Each tile has one contour layer of linestrings tagged with their elevation (ele) and whether they are a major line (major), with the interval per zoom set by zoom_intervals.
Unlike hillshade the output is vector, so the client styles and labels the lines itself.
See the documentation.
Done in #3183.
Added
- JPEG XL:
jxltiles are detected and decoded (viajxl-oxide), and hillshade can encode its output as lossless JPEG XL (viazune-jpegxl), which is usually smaller than PNG or lossless WebP (#3181) - (cache-control) per-source
Cache-Controloverrides on top of the server-wide default. A composite request only uses one if all of its sources are configured with the same value (#3167, #3148) - (postgres)
--ssl-certand--ssl-keyflags for the client certificate and key, mirroring--ca-root-file(#3150 by @reddynitish) - (config) warn once at startup when Martin falls back to a legacy Postgres env var (
DATABASE_URL,DEFAULT_SRID,PGSSLCERT,PGSSLKEY,PGSSLROOTCERT), naming the config key that replaces it. All five keep working, the removal is deferred to v2 (#3151 by @reddynitish)
Fixed
- (mbtiles) avoid a full tiles scan when computing min/max zoom. Startup on a 5M tile file drops from 367ms to 203ms flat, and from 1650ms to 203ms normalized (#3185)
- (catalog) serialize the catalog in a stable key order, so
/catalogno longer differs between restarts (#3189) - (fonts) redirect glyph URLs ending in
.pbfinstead of answering 404, so OpenMapTiles styles work unedited (#3187) - (pmtiles) pick up the ECS and EKS credential env vars, so S3 sources use the task role on Fargate and EKS (#3165)
- (reload) keep per-source
convert_to_*overrides for file-backed (#3160) and Postgres (#3144) sources - (config) gate the
resolve_process_configimports on_process(#3191) - fix cors logging logging
unrecognizable(#3136) - (deps) update npm dependencies (#3177, #3175)
Other
- (pmtiles) open file sources concurrently and share one client per store (#3131)
- Reload and config refactors: the reloader is now the single writer of Postgres sources, the tile reloaders moved into
TileReloaders, file source ids resolve before the sources open, andDiscovery::discoverreturns its warnings (#3130, #3178, #3190, #3133, #3142, #3143) - Split
MartinErrorinto separate error types (#3162) - Docs: a postprocessing section for the two new guides (#3139, #3140), authenticating requests with nginx
auth_request(#3184), typo fixes (#3147 by @vaibhav8a) - Lints and tests: warn on
wildcard_enum_match_arm, adopt clippy 1.98, useassert_matches!, test cleanup and formatting (#3168, #3137, #3166, #3179, #3135) - CI: the save-config e2e tests moved to rust and
tests/test.shis gone, the bundled DuckDB builds once and without debuginfo, build caches and duplicated job setup were cleaned up, and a few flaky tests were fixed (#3105, #3186, #3169, #3157, #3161, #3152, #3145, #3132, #3149, #3146) - (deps) cargo dependencies, docker images, github actions and pre-commit autoupdates (#3172, #3174, #3171, #3173, #3176, #3155)
1.14.0 - 2026-08-17
Security
Two vulnerabilities reported through our security advisories are fixed in this release:
- Out-of-gamut CIE colors in the static-overlay body could permanently kill render workers (GHSA-6774-6cqw-f586, high severity, only affects deployments with the opt-in, Linux-only
renderingfeature enabled). An unauthenticatedPOST /style/{id}/static/...body could carry a CSS color such aslab()oroklch()whose channelscsscolorparserdoes not clamp to0..=1. That out-of-gamut value reached an unchecked assertion deep in the renderer, permanently killing one of martin's 2-8 render workers per request; a handful of ~180-byte requests could disable static rendering for the life of the process. Fixed by clamping colors to the valid gamut before they reach the renderer. Done in #3124. - Repeated sprite ids let a single request amplify into unbounded rasterization work (GHSA-5x5g-6p4c-jqfh, medium severity).
/sprite/{ids}.pngrasterized and cached one SVG per id in the comma-separated id list, with no cap on id count and no deduplication, so repeating a single valid id let an unauthenticated client multiply server work at a ratio of its own choosing. Fixed by capping ids per request, deduplicating before doing the work, and bounding rasterization concurrency. Done in #3123.
Added
- e2e local geoparquet wiring via duckdb (#3054)
Fixed
- install configuration in /etc for debian package (#3078)
- log the resolved listen address on startup (#3053)
- (geojson) advertise vector_layers in TileJSON (#3082)
Other
- migrated the COG source, MBTiles pack/unpack, and the remaining rendering e2e tests to Rust, dropping mitmproxy in the process (#3104, #3101, #3064)
- replace Codecov with GitHub-native code coverage (#3108)
- enable additional clippy restriction lints (#3095)
- Change dynamic brotli encoding level from 11 to 4 (#3061)
- (deps) update dependency lucide-react to v1.27.0 (#3096)
- (deps) update npm dependencies (#3083)
- (deps) Update hotpath and instrument decoders (#3059)
- (deps) dependency bumps and pre-commit autoupdates (#3110, #3109, #3100, #3099, #3098, #3071)
1.13.0 - 2026-07-25
Passthrough sources: config-file wiring
1.12.0 introduced passthrough sources at the martin-core level; this release finishes the job by wiring them into martin's config file.
This adds a dedicated typed config section, clearer errors for unrecognized keys, and correct etag-suffixing so cached passthrough tiles don't collide with tiles from other sources.
Done in #2910.
New cache MBTiles schema
MBTiles files can now use a cache schema: like flat, but with fetched, expires and etag columns stored alongside each tile, so a .mbtiles file can double as a persistent, inspectable web-tile cache.
It still exposes a spec-compatible tiles view, so any standard MBTiles reader (including martin itself) can serve a cache file unmodified.
The mbtiles CLI gained a matching cache-purge <file> [--max-size <MB>] subcommand to evict expired (and, if needed, soonest-expiring) entries and reclaim space.
Done in #3009.
martin-cp now responds to Ctrl+C when copying from PostgreSQL
Previously, interrupting a martin-cp run against PostgreSQL could hang instead of exiting.
In-flight Postgres queries are now tracked and cancelled on interrupt, and the copier drains already-queued tiles instead of getting stuck.
Done in #2901.
Added
- (mbtiles) read and validate the
hash_algorithmmetadata key, so files hashed with an algorithm other than MD5 (e.g. bytippecanoe) report a clear "unsupported algorithm" error instead of a misleading hash mismatch during validation (#2985)
Fixed
- (pmtiles) restore AWS profile support (#3029)
- (deps) update npm dependencies (#3012)
- (martin-ui) center tile inspect map on tileset bounds, not 0/0/0 (#3002)
- (postgres) clearer error when a source schema exists but has no tile sources (#2979)
- (martin-ui) match metric endpoints when a route_prefix is set (#2986)
- connect to prerelease PostgreSQL versions (#3024)
- (postgres) honor sslmode=verify-ca by skipping only the hostname check (#2987)
- (mbtiles) validate min/max zoom metadata against actual tiles (#2983)
Other
- (deps) Bump the npm_and_yarn group across 2 directories with 1 update (#3045)
- (deps) update dependency postcss to v8.5.18 [security] (#3044)
- (deps) autoupdate pre-commit (#3040)
- use re-exported mvt from mlt (#3037)
- derive CollectUnrecognizedKeys instead of hand-written impls (#3031)
- make UnrecognizedValues a newtype (#3030)
- (deps) update cargo dependencies (#3011)
- (deps) update dependency pbf to v5.1.1 (#3020)
- (deps) update dependency @types/node to v26.1.1 (#3019)
- add render_finalize_failure helper and snapshot tests (#3000)
- (deps) autoupdate pre-commit (#2998)
- (postgres) use NonZeroUsize for pool_size (#2996)
- run
just fmt(#2981) - (deps) lock file maintenance (#2961)
- split file sources into MBTiles and PMTiles subpages (#2978)
- update fast-mvt and other deps (#3028)
- update sqlite queries (#3003)
- address feedback from fast-mvt migration (#2993)
- migrate geozero to fast-mvt, make duckdb non-dflt (#2991)
- fix clippy lints for 1.97 release (#2989)
- more sqlx precompiled stmts (#3010)
- cleanup normalized-dedup sql (#2990)
- reuse .sql schema files in code (#2988)
- (mbtiles) refactor schemas to individual files (#2982)
1.12.0 - 2026-07-07
GeoJSON tile source
Martin can now serve vector tiles directly from a .geojson/.json file, no pre-processing into MBTiles/PMTiles required.
How it works:
- On startup the features are reprojected from WGS84 (EPSG:4326) to Web Mercator (EPSG:3857) and indexed in a packed Hilbert R-Tree.
- Per tile request Martin queries the R-Tree for geometries overlapping the tile bbox, clips them (with a configurable
buffer), transforms into tile coordinate space and encodes MVT, all on the fly. The tileextentandbufferare configurable.
Because it's file-backed, GeoJSON sources hot-reload: point Martin at a directory and it watches for .geojson/.json changes via filesystem events, adding, updating and removing sources without a restart, just like PMTiles and COG.
Great for smaller datasets and quick prototyping. Done in #2538, #2925 by @thomfuhrmann.
Static map overlays
Building on the static rendering core from the last release, we added an endpoint to render a static map image with overlays drawn on top:
The camera can be a center point, a bounding box, or auto-fit to the overlays you pass in. Overlays support fills, lines and circles with configurable color/opacity. See the documentation for the full field list and examples. Done in #2794, #2922.
mbtiles pack and mbtiles unpack
The mbtiles CLI gained two subcommands for converting between an MBTiles archive and a {z}/{x}/{y}.{ext} directory tree.
unpack extracts an archive into a directory (handy for inspecting individual tiles or serving them with any static HTTP server), and pack does the inverse.
Done in #2199 by @JakeLow.
Passthrough sources
Martin can now proxy tiles from an upstream HTTP tile server. It fetches each tile from the configured upstream and serves the bytes verbatim, while the rest of Martin's pipeline (caching, headers, MVT<->MLT conversion) applies on top. Use it to put Martin's cache in front of an existing tile server, hide an upstream API key from browsers, or spread requests across mirrors.
passthrough:
sources:
osm: https://tile.openstreetmap.org/{z}/{x}/{y}.png
secure:
url: https://api.example.com/{z}/{x}/{y}
headers:
Authorization: ${API_TOKEN}
timeout: 30s
An upstream can be a {z}/{x}/{y} template, a TileJSON document URL, a list of mirror templates, or the detailed object form above.
See the documentation for the full field list.
Done in #2913, #2908, #2924.
More capable config parsing
We reworked how config files are parsed by migrating YAML handling to serde-saphyr (dropping serde_yaml).
YAML anchors and aliases now work, and the ${VAR:default} (with all the docker-compose shorthands now also supported) environment-variable substitution syntax is still supported.
Done in #2943, #2877, #2870, #2921.
☀️ GSoC spotlight: duckdb GeoParquet sources
GeoParquet (via DuckDB) config parsing and resolver wiring landed as a building block, continuing our GSoC DuckDB work. Top-level config/lifecycle wiring is planned for a follow-up. Done in #2931, #2902 by @manbhav234.
Fixed
- (pg) Redact the password in the
BadConnectionStringerror so it can't leak into logs (#2944). - Improve logging when a requested zoom is out of range (#2893).
Other
- Documented the new passthrough and GeoJSON sources, plus assorted docs fixes (#2965).
- Migrated dependency automation from Dependabot to Renovate (#2947, #2949, #2951, #2952, #2960, #2962, #2967, #2969).
- Numerous CI stability fixes (#2920, #2916, #2963, #2903, #2909)
- various dependency bumps (#2899, #2955, #2966, #2906, #2933, #2932, #2940, #2900)
1.11.0 - 2026-06-16
Live reloading for PostgreSQL, PMTiles and COG sources
Back in 1.8.0 we taught Martin to hot-reload MBTiles directories.
This release finishes the job: PostgreSQL, PMTiles and COG sources now reload automatically too, so Martin keeps serving fresh data without a restart.
Two reload mechanisms are used, depending on what the source can observe:
- File system events - local PMTiles and COG files reload as soon as the underlying file changes on disk (#2750, #2858).
- Fixed timer (polling) - PostgreSQL and remote PMTiles (object stores) can't watch the file system, so Martin polls them on a timer (configurable, default 10m) and reloads when it detects a change. PostgreSQL picks up new and changed tables/functions as they appear, and remote object stores now track source data versions to know when to reload (#2841, #2864).
Under the hood, every source type now shares a common ReloadDriver/Discovery mechanism, which should make future source types easier to support. (#2835, #2836, #2837, #2886, #2844, #2845, #2840, #2852, #2853, #2861, #2867).
☀️ GSoC spotlight: a new duckdb source (in martin-core)
A warm welcome to Manbhav Sugla (@manbhav234), our Google Summer of Code student! This release lands his first PR, kicking off a brand-new DuckDB source for Martin and laying the groundwork for serving tiles from DuckDB.
It is still early and not currently entirely plumbed through the system, but it is a good start. Done in #2831.
Static rendering core
We added the core machinery for static (single-image) rendering.
This allows you to do static rendering such as for example this OG-image showing the :
Done in #2804.
Fixed
- (martin) Static rendering now rejects invalid scale values and inverted bounding boxes instead of producing broken output (#2830).
- (pg) Use
ST_EstimatedExtentfor faster bounds calculation at startup (#1220). - (sprites) Ignored directories are now skipped during sprite resolution (#2815).
- (unstable-cog) Allow up to 0.1% tolerance (up to 3m of error at z0) when matching a COG's zoom level to WebMercatorQuad (#2878).
Other
- The MapLibre Native renderer is now multi-threaded and was updated to
maplibre_native@8.1(#2826, #2859). - A large refactor split the config code into dedicated
config, parsing and lifecycle modules, added tests for exactly how config parsing behaves, and moved test helpers totest_support(#2874, #2875, #2876, #2872, #2885). martin-tile-utilsnow has#![forbid(unsafe_code)], error types with multiple identical fields got names, import ordering drift was fixed, and a pre-commit hook simplifies UTF characters (#2847, #2832, #2838, #2827).- Render fixtures are now cached in CI (#2828).
- (deps) Updated
sqlxto 0.9.0 (adapting MBTiles dynamic SQL toSqlSafeStr), refreshed the lockfile, autoupdated pre-commit, and bumped npm dependencies (#2821, #2822, #2849, #2896, #2856, #2889).
1.10.1 - 2026-05-19
Fixed
- Do not use heavyweight compression with MLT (#2813)
Other
- share code across the resource caches (#2810)
1.10.0 - 2026-05-16
Added
- add if rendering is active to the catalog (#2795)
- Expand relative URLs in style response (#2801)
- (sprites) warn early and explain
.svg-only requirement (#2793)
Other
- tests fail when terminal is not exactly 80 columns (#2803)
- (ui) generate
types.gen.tsfrom OpenAPI and adopt openapi-fetch (#2797) - make sure that unused variables are handled for MLT configuration (#2808)
- various minor docs improvements (#2807)
- fix all m-dashes to use regular dashes (#2805)
- (deps) autoupdate pre-commit (#2799)
1.9.1 - 2026-05-09
Other
- fix cargo-binstall metadata and add a test in CI (#2789)
- add a data underlay for the inspector (#2782)
1.9.0 - 2026-05-06
MVT - MLT pre-processing encoding
Martin can now serve MLT (MapLibre Tiles) by transcoding MVT on the fly.
MLT is a columnar successor to MVT - for our test fixture it is roughly 39% smaller on the wire and 12% faster to serve from cache due to this size difference.
No tile re-generation or schema migration is required:
the conversion runs at request time when the client sends Accept: application/vnd.maplibre-tile.
We also can convert MLT tiles back to MVT if the client requests it.
The mlt cargo feature is now part of the default feature set, so prebuilt binaries ship with MLT support.
Builds without mlt will return an error if a client requests MLT.
You can configure this behavior with the new convert-to-mlt or convert-to-mvt config key.
It accepts three states (auto, disabled, or an explicit encoder object) and can be set at three nesting levels (global, source-type, individual source).
The most-specific level wins and the default is auto.
convert-to-mlt: auto
convert-to-mvt: auto
postgres:
connection_string: postgresql://localhost/mydb
pmtiles:
sources:
basemap:
path: /data/basemap.pmtiles
# inherits the global `auto`
legacy:
path: /data/legacy.pmtiles
# this one source always serves MLT and configures how to serve it
convert-to-mlt:
tessellate: true
convert-to-mvt: disabled
To override the encoder defaults (rarely needed; see the docs for the full field list), pass an explicit object:
convert-to-mlt:
tessellate: false # Enable if your client supports pre-tessellated polygons and you benchmarked that this improves your use case
try_spatial_morton_sort: true # Disable if your data is already spatially ordered
try_spatial_hilbert_sort: true # Disable if Morton sort doesn't compress well for your data
try_id_sort: false # Enable when features have sequential IDs and spatial sorting isn't beneficial
allow_fsst: true # Disable to reduce search space
allow_fastpfor: true # Disable to reduce search space
allow_shared_dict: true # Disable to reduce search space
A full guide is available here. Implemented in #2769 #2773 and #2775
Improved sprite and font previews in the Web UI
The Web UI now renders sprites and font previews directly using MapLibre GL and SDF glyph data.
Sprite catalogs support live PNG/SDF rendering, icon scaling, SDF tint and halo previews, and quick sprite ID copying.
Font cards now render real sample text from /font/{name}/0-255 instead of placeholder previews, with graceful fallback handling when glyph loading fails.
Existing sprite and font endpoints are unchanged. (#2774, #2772)
Structured logging for martin-core and mbtiles
martin-core and mbtiles now emit structured tracing fields instead of interpolated log strings.
Human-readable logs remain unchanged, but external log scrapers that rely on the old free-text format may need updates. (#2777, #2778)
Fixed
- (mbtiles) Prevent folder sources from disappearing when a single MBTiles file fails to initialize. Invalid files now emit a warning while the remaining sources continue loading normally. (#2768)
Other
1.8.2 - 2026-04-29
Added
Added
- We now publish an JSONSchema for our configuration and OpenAPI documentation for our HTTP API (#2760)
Fixed
- The last release had some artifact not get attached, so this release fixes this
Other
- add debug-only
#[tracing::instrument]to hot-path entry points (#2759) - (deps) Bump the all-npm-version-updates group across 2 directories with 5 updates (#2756)
- (mbtiles) migrate from log/env_logger to tracing (#2755)
1.8.1 - 2026-04-29
Fixed
- (styles) default the optional
renderingfield so configs without it parse (#2752)
Other
- (deps) autoupdate pre-commit (#2743)
- (deps) Bump the all-npm-version-updates group across 2 directories with 6 updates (#2745)
- update Cargo.toml dependencies
1.8.0 - 2026-04-28
Added
- Add MbtilesReloader (Tile Reload Phase 2) (#2717)
MBTiles auto-reloading
Previously, if you configured an directory with us, we would list it once and then serve this. This is no longer the case, we now can hot-reload (including updates clearing a sources cache) mbtiles (!). Work towards PMtiles, PG or COG is underway, but if you want this faster a PR to finish the plumbing would be appreciated.
Non-vector PG sources
Some users, might want to serve non-MVT sources from postgres.
We now support this via the content_type in PostgreSQL function source SQL comments.
For further information see our docs or #2671
Other
- remove a few unused deps from Cargo.toml to not waste time building them (#2738)
- update Cargo.toml dependencies
1.7.0 - 2026-04-23
martin_tile_cache_requests_total and martin_cache_requests_total metrics
We have added the following metrics, allowing for knowing what your cache hit rate. These are two metrics because for tiles we include the zoom while for fonts/sprites this does not make sense.
# HELP martin_cache_requests_total Martin cache lookups, labeled by cache type and hit/miss result
# TYPE martin_cache_requests_total counter
martin_cache_requests_total{cache="font",result="miss"} NUMBER
martin_cache_requests_total{cache="sprite",result="miss"} NUMBER
# HELP martin_tile_cache_requests_total Martin tile-coordinate cache lookups, labeled by cache type, hit/miss result, and zoom
# TYPE martin_tile_cache_requests_total counter
martin_tile_cache_requests_total{cache="tile",result="hit",zoom="0"} NUMBER
martin_tile_cache_requests_total{cache="tile",result="miss",zoom="0"} NUMBER
[!TIP] If you have concrete needs for what metrics you would like to see, please open an issue. The set of metrics we offer is quite early in its development lifecycle.
Stabilized Server-side raster tile rendering backend
We have stabilized our rendering backend, which means that you can now render images using MapLibre Native. We have some work planned to improve performance by prefetching and better parallelism, or to add capabilities like overlaying lines/text/shapes.. via query params. If you have needs/interests towards this area, we would also invite you to open a discussion/issue on the API that you would like to see. If you need configurability, we would also like to know what kind of configurability you need.
To enable this feature, you need to add the following to your configuration file:
Added
- (ui) Add Tile URLs TileJSON and XYZ Tiles URLs to the inspect UI (#2731)
- (mbtiles) add --strict flag to use STRICT SQLite tables (#2712)
Fixed
- Keep /health available with
--route-prefix fooinstead of just moving it to /foo/health to enable docker healthchecks (#2723)
Other
- Some refactorings to increase CI reliability (#2724, #2715, #2725)
- (deps) autoupdate pre-commit (#2720)
1.6.0 - 2026-04-18
Smarter, more configurable caching
The tile cache received several improvements in this release:
- Configurable cache expiry
The in-memory tile cache Time To Live (TTL -> e.g.
cache.expiry: 1h) and Time To Idle (TTI -> e.g.cache.idle_timeout: 20m) was previously hardcoded to "∞" (aka never expiring). You can now configure how long cached tiles stay in memory, allowing better trade-offs between freshness and performance for your specific workload.
cache:
# Maximum lifetime for all cache entries (time-to-live from creation).
# Entries are evicted after this duration regardless of access.
# Supports human-readable formats: "1h", "30m", "1d", "3600s".
# default: null (no expiry, entries only evicted by size pressure)
expiry: null
# Maximum idle time for all cache entries (time-to-idle since last access).
# Entries are evicted if not accessed within this duration.
# default: null (no idle timeout)
idle_timeout: null
Done in #2691.
- Per-source cache zoom levels
New cache.minzoom and cache.maxzoom options (both globally and per-source) let you skip caching at zoom levels that don't benefit from it.
For example, you can avoid filling the cache with rarely-reused high-zoom tiles or low-detail overviews.
cache:
# Default minimum zoom level (inclusive) for tile caching.
# Tiles further zoomed out than this will bypass the cache entirely.
# Can be overridden per-source (e.g. cache.minzoom on a type of source or an individual source).
# default: null (no lower bound, all zoom levels cached)
minzoom: null
# Default maximum zoom level (inclusive) for tile caching.
# Tiles further zoomed in than this will bypass the cache entirely.
# Can be overridden per-source.
# default: null (no upper bound, all zoom levels cached)
maxzoom: null
Done in #2673 by @carderne.
- Cache deduplication under concurrency
Cache insertions now use moka's entry API, so concurrent requests for the same tile only compute it once instead of redundantly.
This is a meaningful performance win under thundering-herd scenarios.
Done in #2688.
- Accept header in cache key -- The sanitized Accept HTTP header is now part of the cache key, preventing a cached response encoded for one client from being incorrectly served to another.
This previously did not have any effect and was also not incorrect, but in the next release we will add MLT encoding support (which we worked hard for).
This also has the side-effect that if your client now says that you only Accept a certain format, we now correctly abort requests early.
Done in #2703.
Broader MBTiles compatibility
- Planetiler
normalizedschema alias -- Martin now recognizes Planetiler'snormalizedandnormalized-with-viewschema names as aliases for its ownnormschema type, so MBTiles files produced by Planetiler no longer trigger schema-detection warnings. Done in #2681. - Compression type stored in metadata -- When writing tiles to MBTiles (e.g. via martin-cp), the compression method (gzip, brotli, etc.) is now recorded in the metadata table. Previously this information was lost, forcing consumers to guess. Done in #2618.
- Transcoder API for library consumers -- The
mbtilescrate now exposes a public API for converting between MBTiles storage schemas (flat, normalized, deduplicated) programmatically. Done in #2682.
--on-invalid CLI argument
The on_invalid setting (which controls whether Martin warns or aborts when it encounters an invalid source at startup) was previously config-file-only.
It is now available as --on-invalid <warn|abort> on the command line, which is especially handy in CI/CD and container environments.
Other
- Introduced
TileSourceManagerandReloadAdvisoryas groundwork for future live-reload of tile sources (#2661) by @Auspicus - Added hotpath-based profiling integration (#2663)
- Enabled React Compiler for martin-ui and demo frontend (#2686)
- Enabled
clippy::unwrap_usedworkspace lint (#2670) - Ensured unit tests run on macOS (#2648) by @Weixing-Zhang
- Various dependency bumps (#2702, #2684, #2624, #2662, #2657)
1.5.0 - 2026-04-02
Fixed
- (postgres) startup crash when ST_Extent returns LineString instead of Polygon (#2600)
Other
- typos (#2651)
- migrate to workspaced justfiles using
modfor demo and martin-ui (#2623) - (deps) Bump the all-npm-security-updates group across 2 directories with 1 update (#2647)
- Enable
clippy::use_selfat workspace level and resolve all violations (#2645) - (deps-dev) Bump flatted from 3.3.3 to 3.4.2 in /martin/martin-ui in the all-npm-security-updates group across 1 directory (#2640)
- (perf) don't test pg twice (#2619)
1.4.0 - 2026-03-14
ZSTD support
If your browser prefers this, we will now start sending ZSTD (or deflate) compressed tiles your way. Done in #2597 by @nuts-rice
A new documentation site
We migrated our documentation to zensical, a more modern documentation platform. Just have a look for yourself, does it not look pretty? -> https://maplibre.org/martin Done in #2576 by @manbhav234
Added
- (martin-cp) now has a prettier, indicatif based progress bar (#2495)
- Add retry mechanism on locked/busy mbtiles files was added (#2572)
Fixed
- (ui) render MLT tiles correctly in Tile Inspector (#2601)
- redirect ignoring
--route-prefixfor .pbf tile requests (#2599) - restrict zooming and panning on data inspector (#2574)
- Accept any INT-containing type in MBTiles validation to be an integer (#2560)
Other
- rename the
webp.sqlfixture towebp-no-primary.sql(#2564) - more cfg magic instead of #[allow(unused_variables)] (#2563)
- More restrictive expects (#2562)
- feature-gate PostgreSQL tests to remove external dependencies from
cargo test(#2558) - Bump some dependencies (#2608, #2602, #2592, #2577, #2575, #2567)
1.3.1 - 2026-02-11
Added
- (srv) Add HTTP 301 redirects for common URL mistakes (#2528)
- (unstable-cog) Change tile path semantics for COG sources to match other sources, expose COG bounds, center and tileSize in TileJSON (#2510)
Fixed
Other
- Add test coverage for header handling in tilejson requests (#2529)
- (martin-core) [breaking] remove the configuration from the martin-core crate (#2521)
- restrict
unused_trait_namesfor trait imports (#2542) - (deps) Bump various dependencies (#2553, #2545, #2533)
1.3.0 - 2026-01-27
More flexible log formatting
We migrated our log library to tracing.
This gives us a few internal improvements, but also allows us to introduce a new RUST_LOG_FORMAT environment variable.
The available values are: json, full, compact (default), bare or pretty.
Done in #2494, #2508, #2500 by @CommanderStorm
Glyph ranges beyond 0xFFFF
If you are using fonts which span beyond the 0xFFFF range, this release improves how Martin loads and renders those glyphs so they are handled correctly.
Here is a short explanation of why this might matter to you based on https://en.wikipedia.org/wiki/Unicode_block.
U+0000-U+FFFFis Basic Multilingual Plane, which covers characters for almost all modern languagesU+10000-U+3347Fcovers minor characters such as historic scripts and emojisU+E0000-U+E01EFis for tags and variation selectorsU+F0000-U+10FFFFis for private use (i.e. can be assigned arbitrary custom characters without worrying about possible conflict with the future standards)
Done in (#2438) by @yutannihilation
As a related performance optimization, we also removed FontSources.masks as it was consuming large amounts of memory and some startup time, even when no font sources were set (#2519) by @Auspicus
Simpler native subpath support
We added the route_prefix configuration and --route-prefix cli arguments.
This allows you to configure the subpath martin is serving from without the need for your reverse proxy to strip these subpaths before getting to us.
Done in (#2523)
MLT decoding support
Martin now supports the MapLibre Tiles Specification. This means that if you want to serve MLT based tiles with this tileserver, you now can. Read more about what the MapLibre Tile Specification is and why we are "reinventing the wheel on this one" in our blog post.
Done in (#2512)
Added
- improve martin-cp progress output time estimate by displaying in human time instead of seconds (#2491)
- (pg) support PostgreSQL materialized views (#2279)
- (pg) include ID column info for tables (#2485)
Fixed
- improve error message if no SVG sprite files are present (#2516)
- (ui) Fix clipboard copy for http://0.0.0.0:3000 and unify implementations and their design (#2487, #2489, #2483, #2482)
Other
- (deps)
cargo-shearour dependencies for improved compile times (#2497) - (mbtiles) improve a few test cases (#2478, #2480, #2477)
1.2.0 - 2026-01-03
Optionally fail config loading/resolution for missing sources
We added the on_invalid: abort (default) and on_invalid: warn settings, which controls what happens when martin encounters an missing/invalid source.
Done in #2412, #2426 by @gabeschine
Click to copy when clicking on various IDs in the UI
When clicking on various IDs in the UI, a click to copy feature is now available.
Done in #2427 by @todtb
Fixed
- (pg) Instead of reporting on all available tables, we now filter the result to the configured sources when
auto_publish: false(#2411) - (sprites) Scale SDF buffer and radius by pixel ratio leading to weird artifacts when using retina sdf sprites (#2458)
Other
- made our dependency management more reproducible/stable (#2442, #2429, #2415)
- various dependency bumps (#2439, #2435, #2418, #2471)
- (bench) improve benchmark accuracy by adding black_box for tables/functions (#2413)
- (pmtiles) add pmtiles test in
martin-core(#2443)
1.1.0 - 2025-12-11
Added
- (martin-cp) infer default bounds from configured sources for better performance (#2385)
- (martin-core) add an pmtiles example (#2370)
Fixed
- allow
az://URL schemes in discovery (#2408)
Other
- (config) move the resolve impl to a different function (#2397)
- (docs) fix martin-cp bbox docs (#2387)
- (deps) miscellaneous dependency bumps (#2403, #2404, #2373, #2375, #2374)
1.0.0 - 2025-11-10
🎉🎉🎉 After 8 years in development, we are excited to release v1.0.0 of martin. 🎉🎉🎉
Functionally, it is the same as v0.20.2, just with our releases further automated.
There are no breaking changes between v0.20.X and v1.X.X
Fixed
- broken url to github release in web-ui (#2354)
0.20.2 - 2025-11-07
In 0.20.1 we claimed to have fixed the bug regarding how our release script determines versions for docker containers.
This was incorrect and is fixed now with a more manual approach instead of relying on docker/metadata-action.
Done in #2348
Other
- Remove unused optional 'tiff' dependency from Cargo.toml (#2343)
0.20.1 - 2025-11-03
Fixed prefixes in ghcr tags
We fixed a bug where in the 0.20.0 release our ghcr.io tags always had the prefix :martin-v0.20.0 and were also published under :martin-core-v0.2.0 and :mbtiles-v0.14.0.
Sorry for users affected by this change. Done in #2338
Fix
Fixed a potential crash due to an off-by-one error when zooming in at exactly Zoom 30 (our limit). #2340
Maintenance
- (ci) add pre commit step to sync the fronted version to the backend (#2324)
- reduce pg discovery bench sizes (#2321)
- various dependency bumps (#2331, #2333, #2332)
0.20.0 - 2025-10-27
[!NOTE] This release can be considered the last beta of the v1.0 release. We have locked down key parts of the architecture.
We will republish this release as v1.0 in roughly a week, unless we see any bugs in this release.
A big thank you to everyone who contributed to this release - through code, reviews, testing, and feedback. Your work and discussions continue to make Martin faster, more reliable, and more welcoming for new users.
We couldn't have done it without you ❤️
A better, more configurable cache
In previous versions, the cache was a single monolithic cache. We have split this up into different parts and you can now specify how much sprites, fonts, pmtiles directories and tiles martin is allowed in the cache.
[!TIP] We also now support caching sprites and fonts - speeding up the rendering of vector maps.
See our documentation here for further context.
Done in #2295 #2304 #2303, #2297
Pmtiles support for Google Cloud, Azure and much more options
The good news first: - greatly expanded options for AWS and HTTP backends - New support for Google Cloud and Azure object storage - Local files remain unaffected
How did we do this?
We replaced our entire pmtiles backend with the object_storage crate.
Most of the options are cleanly migratable, but we deprecated the following:
- AWS specific environment variable usages are deprecated.
pmtiles.allow_httpbeing unset is currently defaulting totrue. In v2.0, we will change this to befalseby default for better security defaults.
The deprecated items will be removed in v2.0 at the earliest.
[!TIP] Each of the deprecations also has its own warning in the log, so you don't have to guess if you are affected 😉
AWS_PROFILE presented a challenge and we had to drop this environment variable.
We asked for community feedback on Slack (see here), and it seems this may not be a necessary feature.
If you depend on AWS_PROFILE, we opened the following issue to discuss details:
- https://github.com/maplibre/martin/issues/2286
For further details on the now available options, please see our documentation.
Done in #2251
unstable style rendering support
We added an experimental option for server-side style rendering, allowing you to convert your configured styles into images on the server side instead of the client. See our documentation here for further context.
Done in #2306
rename cog feature to unstable-cog
The cog feature was renamed to unstable-cog and thus removed from the features active by default.
If you compile martin from source with this feature enabled, experimentation is still possible.
This change signals that the feature is still evolving and allows us to iterate more freely as we add the missing functionality.
Currently, our COG support does not support certain projection aspects required for good usability.
Done in #2285
Removal of deprecated functionality
We removed the long-deprecated --watch CLI option, which previously only displayed a deprecation warning in the log.
Done in #2294
Fix
- Make mbtiles dependency properly optional again (#2292)
Other
- (core) enable overriding of the automatic hashing for source traits (#2293)
- (pg) Add benchmark for source discovery timing (#2263)
- (admin) move functionality into better modules (#2315)
- move config files to new folders (#2298)
- (core) add a
_tilesfeature to simplify our feature configuration (#2296) - (config) refactor the lifecycle hooks to be cleaner and better documented (#2282)
- (lints) apply tighter clippy lints like
clippy::panic_in_result_fn,clippy::todoor similar [#2284](https://github.com/maplibre/martin/pull/2284 #2283, #2288, #2287 - (mbtiles) Generate mbtiles dynamically from SQL files to increase debuggability, transparency and supply chain trust/security (#1868)
- A number of dependency updates #2277, #2308
0.19.3 - 2025-10-01
Added
- add
tilejson_url_version_paramconfiguration which allows embedding the version of tile sources (specifically pmtiles) in tilejson tiles URL, resulting in better cache hit rates (#2198)
Other
- fix docs.rs not build failing due to misconfiguration of the
cfg(feature="webui")(#2273) - release (#2265)
0.19.2 - 2025-09-28
Other
- The previous
0.19.1release had another bug which prevented proper releasing which is now fixed (#2262) - update Cargo.lock dependencies
0.19.1 - 2025-09-28
Fixed
- (release) Our release new process for
0.19.0did not properly attach binaries and build docker files due to permission issues (#2253, #2260)
0.19.0 - 2025-09-26
Breaking Changes
- we migrated our internal codebase to be split into
martin-coreandmartin. While this does have NO have an public facing impact for API, Configuration and behavior, this ensures that we can release v1.0 without breaking the SemVer promise. If you previously usedmartinas a crates.io library, please usemartin-coreinstead. (#2227,#2215,#2217,#2213,#2216,#2192,#2194,#2191,#2185,#2182,#2181,#2184,#2179,#2176,#2178,#2177,#2172,#2171,#2167,#2158,#2157,#2160,#2156,#2105,#2048,#1944, #2159) - (martin-cp) The
--cache-sizeoption has been removed from martin-cp. For most use cases, this is not what you want. (#2026)
Added
- We updated Martins' Logo (#1959)
- (config) Implement unrecognized value in config file warning (#2151, #2152, #2236, #1967)
- (martin-cp) add a warning if
--concurrency 1and an error if--concurrency 0(#2027)
Fixed
- (docs) publish docs to docs.rs again (#2239)
- (ui) inspect button not working for raster sources (#2155)
- (pg) fixed if one has a table and an identically named view in another schemas, tile serving did not work (#2149, #2112)
Documentation
- reworded the error messages for
InternalError,FontError(#2226) - add doc comments for pmtiles and mbtiles (#2164)
- document public parts of the
cog-module (#2166) - document the exposed parts of the
pgmodule (#2165) - improve
IdResolver::resolvewarnings (#2066) - improve doc comment for
IdResolver::resolveto remove ambiguity if we should log for reserved names (#2065)
Other
- We have automated our release pipeline and are now releasing via
release-plz(#2242) - fix various clippy or related code style issues (#1904, #1903, #2092, #2052, #2193, #2130, #2233)
- Add comprehensive GitHub Copilot instructions for Martin development workflow (#2210)
- (bench) add an benchmark that tests the impact of the error variant (#2168)
- (cog) use binary snapshots for testing (#2129)
- (ui)make sure that the fronted forces esm instead of cjs (#2038)
- (ui) migration
jesttovitest(#2040) - (ui) move from eslint to biomejs for formatting/linting (#1909)
- (ci) add cargo-sort to consistently sort our
Cargo.toml(#2020) - (ci) Split tests and lints in CI (#2225)
- dependencies And a bunch of dependency updates (#2218, #2189), #2161, #2143, #2147, #2139, #2128, #2135, #2125, #2126, #2120, #2119, #2111, #2082, #2093, #2084, #2081, #2069, #2070, #2013, #2106, #2104, #2050, #2049, #2100, #2232)