Skip to content
KanahiroPublic

About

A tiny raster tile server for MapLibre styles.

Topics

Resources

Stars

120 stars

Watchers

0 watching

Forks

Repository files navigation

chiitiler

chiitiler

A tiny raster tile server for MapLibre styles.

Point it at any style.json and get back PNG / WebP / JPEG tiles, static images, or map cut-outs.

Release Unit Tests Integration codecov License

Quickstart · HTTP API · Library · Deployment


From zero to a rendered tile in 30 seconds

1. Run the server — one command, no config file, no database, no API key.

docker run --rm -p 3000:3000 ghcr.io/kanahiro/chiitiler:latest

2. Open a tile in your browser — pass any MapLibre style URL as ?url=.

http://localhost:3000/tiles/0/0/0.png?url=https://tile.openstreetmap.jp/styles/osm-bright/style.json

You're done. That same endpoint works as an XYZ tile source for Leaflet, MapLibre, OpenLayers, QGIS, Cesium, or anywhere else that speaks {z}/{x}/{y}.

No Docker? npx tsx works too:

git clone https://github.com/Kanahiro/chiitiler && cd chiitiler && npm i
npx tsx src/main.ts tile-server --debug

Then visit http://localhost:3000/debug to preview styles interactively.

Features

  • Zero-config — no config file, no YAML, no database. Just a style URL.
  • Works with any MapLibre style — remote URL or POST the JSON inline
  • Multiple outputs — slippy tiles (/tiles), bounding-box clips (/clip), free-form camera shots (/camera)
  • Serverless-friendly — small footprint, runs on AWS Lambda via Web Adapter (see cdk/)
  • Pluggable caching — memory · file · s3 · gcs backends for shared source assets
  • Many protocols — http(s) · s3 · gs · file · mbtiles · pmtiles · cog
  • Library or server — import the renderer directly into your Node.js pipeline
  • Built-in debug UI — /debug and /editor for live style preview

In Production

Presentations

HTTP API

Method Endpoint Description
GET / POST /tiles/{z}/{x}/{y}.{ext} Slippy-map raster tile
GET / POST /clip.{ext} Bounding-box cut-out
GET / POST /camera/{zoom}/{lat}/{lon}/{bearing}/{pitch}/{width}x{height}.{ext} Free-form camera shot
GET /debug, /editor Debug UI (requires --debug)

ext is one of png, jpeg, jpg, webp.

Query parameters

Name Default Notes
url — Style JSON URL (required for GET)
tileSize 512 Tile size in pixels
quality 100 JPEG / WebP quality
margin 0 Tile edge margin
bbox — /clip bounding box: minLon,minLat,maxLon,maxLat
size 1024 /clip longest edge in pixels

For POST, send the style object as JSON body: { "style": { ... } }.

Library Usage

Chiitiler is also published to npm. Returns Buffer or Sharp streams.

import { getRenderedTileBuffer, ChiitilerCache } from 'chiitiler';

const cache = ChiitilerCache.fileCache({ dir: './.cache', ttl: 3600 });

const png = await getRenderedTileBuffer({
    stylejson: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',
    z: 5, x: 27, y: 12,
    tileSize: 512,
    ext: 'png',
    quality: 100,
    margin: 0,
    cache,
});

Available renderers: getRenderedTileBuffer, getRenderedClipBuffer, getRenderedCameraBuffer, and their *Stream variants (Sharp instances for further piping).

For library use, explicitly call prewarm(cache) once per rendering process during application startup, before accepting requests:

import { prewarm, ChiitilerCache } from 'chiitiler';

const cache = ChiitilerCache.noneCache();
await prewarm(cache);

The promise resolves after a minimal tile has been rendered and PNG-encoded. It rejects if warmup fails; the application decides whether to continue startup. Library imports do not automatically run prewarm, and CHIITILER_PREWARM only controls the CLI server. Style-specific tiles, glyphs, and sprites are not preloaded.

Types are re-exported as well, so you can annotate call sites without depending on @maplibre/maplibre-gl-style-spec resolution in your own tree:

import type {
    StyleSpecification,
    GetRenderedTileOptions,
    GetRenderedClipOptions,
    GetRenderedCameraOptions,
    SupportedFormat,
    Cache,
} from 'chiitiler';

Prefer chiitiler's StyleSpecification over importing it from @maplibre/maplibre-gl-style-spec directly: if your tree resolves a different major of that package, the two structurally-different types are not assignable to each other.

chiitiler doesn't override the User-Agent on outbound HTTP requests by default, so the runtime's default is used (Node sends node). Some tile providers (e.g. OpenStreetMap) require an identifying User-Agent — set one with setUserAgent('my-app/1.0') or the CHIITILER_USER_AGENT environment variable.

Configuration

All options can be set via CLI flag or environment variable.

Server

Flag Env Default
--port <n> CHIITILER_PORT 3000
--debug CHIITILER_DEBUG false
--user-agent <ua> CHIITILER_USER_AGENT (none)
--prewarm [true|false] CHIITILER_PREWARM true
--processes <n> CHIITILER_PROCESSES 1 (set 0 for all CPUs)

Prewarm is enabled by default: one tile is rendered at startup before the server starts listening, moving shared renderer initialization work into startup. --prewarm false or CHIITILER_PREWARM=false disables it. --prewarm alone enables it. The CLI flag takes precedence over the environment variable. Style-specific tiles, glyphs, and sprites are not preloaded.

--processes takes precedence over CHIITILER_PROCESSES and accepts non-negative integers; 0 uses all available CPUs.

Cache

Renderer Maps are reused per style and rendering mode without an idle timeout. They are released when their pool is evicted from the 10-entry LRU or explicitly closed. This avoids repeated initialization after quiet periods, at the cost of retaining native renderer memory between requests.

Flag Env Default
--cache <none|memory|file|s3|gcs> CHIITILER_CACHE_METHOD none
--cache-ttl <seconds> CHIITILER_CACHE_TTL_SEC 3600
--memory-cache-max-item-count <n> CHIITILER_MEMORYCACHE_MAXITEMCOUNT 1000
--front-cache-ttl-sec <seconds> CHIITILER_FRONT_CACHE_TTL_SEC 10 (seconds)
--front-cache-max-bytes <bytes> CHIITILER_FRONT_CACHE_MAX_BYTES 67108864 (64 MiB)
--file-cache-dir <dir> CHIITILER_FILECACHE_DIR ./.cache
--s3-cache-bucket <name> CHIITILER_S3CACHE_BUCKET —
--s3-region <region> CHIITILER_S3_REGION us-east-1
--s3-endpoint <url> CHIITILER_S3_ENDPOINT —
--s3-force-path-style CHIITILER_S3_FORCE_PATH_STYLE false
--gcs-cache-bucket <name> CHIITILER_GCS_CACHE_BUCKET —
--gcs-project-id <id> CHIITILER_GCS_PROJECT_ID —
--gcs-key-filename <path> CHIITILER_GCS_KEY_FILENAME —
--gcs-cache-prefix <prefix> CHIITILER_GCS_CACHE_PREFIX —
--gcs-api-endpoint <url> CHIITILER_GCS_API_ENDPOINT —

Chiitiler caches source assets (vector tiles, glyphs, sprites) — not final rasters — so cached data is reused across requests. Standard AWS / GCP credentials (AWS_ACCESS_KEY_ID, GOOGLE_APPLICATION_CREDENTIALS, etc.) are respected.

The tile server adds a per-process memory front cache to file, s3, and gcs: by default, buffers are retained for 10 seconds from insertion, with an LRU limit of 64 MiB of buffer payloads (JavaScript overhead is additional). Reads promote backing-cache hits into memory; writes populate memory immediately and also write to the backing cache. none and memory keep their existing behavior. This affects sources that already use the cache, not direct s3://, gs://, or local source reads.

Set --front-cache-ttl-sec / CHIITILER_FRONT_CACHE_TTL_SEC to a positive, finite number of seconds and --front-cache-max-bytes / CHIITILER_FRONT_CACHE_MAX_BYTES to a positive integer number of bytes to override these limits. CLI flags take precedence over environment variables. Setting either limit to 0 disables the front cache and uses the backing cache directly. Otherwise, invalid values stop startup when using file, s3, or gcs. These settings do not affect none, memory, or library callers.

The front-cache TTL does not guarantee source freshness: promotion can retain a value for up to the configured TTL beyond its backing-cache expiry, and the S3/GCS cache implementations do not check expiry on reads. Each worker has its own memory limit. Library callers can opt in and customize the limits (either limit set to 0 returns the backing cache unchanged):

const cache = ChiitilerCache.withMemoryCache(
    ChiitilerCache.fileCache({ dir: './.cache', ttl: 3600 }),
    { ttlSeconds: 10, maxBytes: 64 * 1024 * 1024 },
);

Deployment

  • Docker — ghcr.io/kanahiro/chiitiler:latest (entrypoint: tile-server)
  • Docker Compose — see docker-compose.yml (includes RustFS + fake-gcs-server for local testing)
  • AWS Lambda — ready-to-deploy CDK app in cdk/

Develop

Requires Node.js 24.12+ and sharp system deps (see Dockerfile).

git clone https://github.com/Kanahiro/chiitiler.git
cd chiitiler
npm install
npm run dev              # tsx watch mode
npm run test:unit        # vitest
npm run test:integration # end-to-end
npm run test:benchmark   # see bench/BENCHMARK.md
npm run build            # bundle to build/main.cjs

Architecture

graph LR
    subgraph sources
        direction LR
        A[style.json]
        B[z/x/y.pbf]
        C[z/x/y.png/webp/jpg]
        D[sprite]
        E[glyphs]
    end

    subgraph chiitiler
        cache
        render
        server
    end

    sources --> cache --> render --> server --/tiles/z/x/y--> png/webp/jpg
    cache <--get/set--> memory/file/s3/gcs
Loading

Credits

Inspired by maptiler/tileserver-gl and developmentseed/titiler.

License

MIT © Kanahiro Iguchi

About

A tiny raster tile server for MapLibre styles.

Topics

Resources

Stars

120 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages