Slice images into XYZ map tiles locally with Rust and WebAssembly or through the hosted processing service.
Tileforge takes a single large image and slices it into a directory tree of small square tiles organized as {zoom}/{x}/{y}.png — the standard XYZ tile format used by Leaflet, Mapbox GL, OpenLayers, and every major mapping library.
Local mode runs the pipeline in a Web Worker using Rust compiled to WebAssembly, so the source image stays on your device. Pro users can explicitly select Server mode for larger jobs; Server mode uploads the source to the Tileforge API for processing and persistent storage.
There's also a native CLI for batch processing and scripting.
- Browser-based — drop an image, configure, download a ZIP of tiles. Zero server uploads.
- Server-side processing — Pro users can offload processing to a native Rust API for larger images and higher zoom levels.
- Rust + WebAssembly — native-speed image processing compiled to WASM via
wasm-pack. - Three processing strategies — automatically selected based on image size:
Strategy When Memory Naive Small images (< 256 MB decoded) Full decode + resize per zoom Streaming PNG Large PNGs Row-by-row decode, never holds full image Strip extraction Large JPEG/WebP Full decode, but no per-zoom resized copies - Flat and Mercator projections — flat/equirectangular for fictional maps and artwork; Web Mercator (EPSG:3857) for real-world geographic maps from equirectangular sources.
- ZIP and PMTiles output — download tiles as ZIP or as a single PMTiles archive for efficient web serving.
- Single-pass processing — TeeWriter generates ZIP + PMTiles simultaneously in one pass over the tiles.
- PMTiles preview — tileset detail page streams only visible tiles via HTTP range requests instead of downloading the full archive.
- Thumbnail generation — worker auto-generates a 480px JPEG thumbnail for each tileset.
- Interactive tile preview — rendered with Leaflet directly from in-memory tiles. Available on both the home page (after processing) and tileset detail pages.
- Tileset gallery — browse public tilesets with thumbnail previews. Manage your own tilesets with rename, visibility toggle, and delete.
- GitHub OAuth — sign in with GitHub. JWT-based auth shared between Next.js and the Rust API.
- API keys — Pro users can generate
tf_...bearer tokens for programmatic access to their tilesets. - Notifications — in-app notification system with DB-backed storage for Pro users.
- Rate limiting — per-IP rate limits on all API endpoints via Redis.
- Account management — deactivation with 30-day grace period and admin purge.
- Configurable — tile size (128/256/512), min/max zoom, projection type.
- Pyramid builder — lower zoom levels are built by merging 4 tiles into 1, cascading from max zoom down to zoom 0.
- CLI tool — native binary for scripting and batch jobs.
- Dark mode — green-tinted dark theme with amber accents (light mode supported too).
tileforge/
├── Cargo.toml # Workspace root
├── crates/
│ ├── core/src/ # Tiling engine (Tiler, ZipTileWriter, PmTilesTileWriter, TeeTileWriter)
│ ├── api/src/ # HTTP API (axum)
│ │ ├── main.rs # Entrypoint, service init, router wiring
│ │ ├── config.rs # AppConfig (env var parsing)
│ │ ├── state.rs # AppState, shared helpers (require_db, S3 cleanup)
│ │ ├── error.rs # ApiError enum + HTTP status mapping
│ │ ├── auth.rs # JWT/API key auth middleware, extractors
│ │ ├── rate_limit.rs # Tiered rate limiting (anon < free < pro)
│ │ ├── s3.rs # S3 bucket initialization
│ │ └── handlers/ # Route handlers
│ │ ├── tiles.rs # Tile processing (sync/async), SSE progress, downloads
│ │ ├── tilesets.rs # Tileset CRUD, PMTiles presigned URLs
│ │ ├── user.rs # Current user, account deactivation
│ │ ├── notifications.rs # Notification CRUD
│ │ ├── api_keys.rs # API key management (Pro only)
│ │ └── admin.rs # Broadcast notifications, purge accounts
│ ├── worker/src/main.rs # Background worker — async jobs, thumbnail generation
│ └── wasm/src/lib.rs # WASM bindings for crates/core
├── cli/src/main.rs # Native CLI binary
└── web/ # Next.js 16 + Tailwind v4 + shadcn/ui
├── app/
│ ├── layout.tsx # Root layout, navbar, SEO metadata
│ ├── page.tsx # Main UI: drop zone, config, progress, download
│ ├── gallery/page.tsx # Public tileset gallery with thumbnails
│ ├── my-tilesets/page.tsx # Authenticated user's tilesets
│ ├── tilesets/[slug]/ # Tileset detail: metadata, PMTiles preview, code snippets
│ ├── settings/page.tsx # User settings: API keys, notifications, account
│ └── billing/page.tsx # Stripe billing portal
├── components/
│ ├── tile-preview.tsx # Leaflet-based in-memory tile viewer (ZIP)
│ ├── pmtiles-preview.tsx # Leaflet tile layer backed by PMTiles range requests
│ ├── api-key-card.tsx # API key generation and display
│ ├── notification-panel.tsx # Notification dropdown panel
│ ├── notification-context.tsx # Notification state provider
│ ├── upgrade-banner.tsx # Pro upgrade prompt
│ ├── navbar.tsx # Site navigation with auth
│ └── user-menu.tsx # GitHub OAuth sign in/out
├── hooks/
│ ├── use-tilesets.ts # Tileset CRUD + PMTiles preview hooks
│ ├── use-api-key.ts # API key management hook
│ ├── use-user.ts # Current user hook
│ └── use-deactivate.ts # Account deactivation hook
├── lib/
│ ├── api.ts # API client (tilesets, keys, notifications, user)
│ ├── notifications.ts # Notification types and helpers
│ └── plans.ts # Plan definitions and limits
└── public/
├── tileforge.worker.js # Standalone Web Worker (importScripts, no bundler)
└── wasm/ # wasm-pack output (--target no-modules)
| Service | Purpose |
|---|---|
| web | Next.js 16 — UI, auth (Auth.js v5), SSR |
| api | Rust (axum) — modular handlers for tiles, tilesets, auth, rate limiting |
| worker | Rust — async job consumer, thumbnail generation |
| NATS | JetStream job queue — durable publish, explicit ack, retry/DLQ |
| Redis | Progress cache + rate limiting (job queue fallback if no NATS) |
| Postgres | Users, tile sets, notifications, API keys |
| S3 | Tile storage (ZIP, PMTiles, thumbnails, uploads) |
uploads/{job_id}.bin # Temporary upload (deleted after processing)
tiles/{job_id}/tiles.zip # ZIP output
tiles/{job_id}/tiles.pmtiles # PMTiles output
tiles/{job_id}/thumbnail.jpg # 480px JPEG thumbnail
Turbopack cannot bundle WASM imports inside Web Workers. The worker and WASM glue are plain scripts in public/, loaded via importScripts(). The React provider loads a versioned worker URL, and the production Docker build compiles and copies the matching WASM artifacts.
- Rust (stable) — rustup.rs
- wasm-pack —
cargo install wasm-pack - Node.js 20+ and npm
- Docker — for Postgres, MinIO, Redis, and NATS
- mprocs —
cargo install mprocs(tabbed TUI for running services) - Stripe CLI —
brew install stripe/stripe-cli/stripe(for webhook testing)
1. Create env files:
.envin the project root with Rust service variables:
DATABASE_URL=postgres://tileforge:tileforge@localhost:5433/tileforge
REDIS_URL=redis://127.0.0.1:6380
NATS_URL=nats://127.0.0.1:4222
JWT_SECRET=<shared-secret>
S3_ENDPOINT=http://localhost:9000
S3_BUCKET=tileforge
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadminweb/.env.localwith Next.js variables:
DATABASE_URL=postgres://tileforge:tileforge@localhost:5433/tileforge
AUTH_SECRET=<random-string>
AUTH_GITHUB_ID=<your-github-oauth-app-id>
AUTH_GITHUB_SECRET=<your-github-oauth-app-secret>
JWT_SECRET=<shared-secret>
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_ID=price_...2. Start everything:
cargo xtask devThis will:
- Start Docker infrastructure (Postgres, Redis, MinIO, NATS) and wait for readiness
- Install web dependencies if needed
- Launch mprocs with tabbed views for api, worker, web, and stripe
Use the mprocs TUI to switch between service logs. Ctrl+q exits and automatically stops Docker containers.
Important: Rust services are compiled with
--release— image processing is 10-100x slower in debug mode.
Open http://localhost:3000.
git clone https://github.com/thesandybridge/tileforge.git
cd tileforgecargo build --releasecd crates/wasm
wasm-pack build --target no-modules --out-dir ../../web/public/wasm --releasecd web && npm install && npm run devLocal processing works without the Rust services. Authentication, Server mode, saved tilesets, and account features require their corresponding database, queue, and object-storage services.
| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
No | 8080 |
HTTP listen port |
MAX_UPLOAD_BYTES |
No | 524288000 (500 MB) |
Max image upload size |
REDIS_URL |
No | — | Redis URL (enables progress + rate limiting) |
NATS_URL |
No | — | NATS URL (enables JetStream job queue; falls back to Redis) |
DATABASE_URL |
No | — | Postgres URL (enables tileset CRUD) |
CORS_ORIGIN |
No | * (permissive) |
Allowed CORS origin |
JWT_SECRET |
No | — | Shared HS256 secret for JWT auth |
S3_ENDPOINT |
No | — | S3-compatible endpoint URL |
S3_BUCKET |
No | — | S3 bucket name |
S3_ACCESS_KEY |
No | — | S3 access key |
S3_SECRET_KEY |
No | — | S3 secret key |
S3_REGION |
No | us-east-1 |
S3 region |
ADMIN_SECRET |
No | — | Secret for admin-only endpoints (purge) |
| Variable | Required | Default | Description |
|---|---|---|---|
REDIS_URL |
No | redis://127.0.0.1:6380 |
Redis URL (progress tracking) |
NATS_URL |
No | — | NATS URL (JetStream job queue; falls back to Redis BRPOP) |
DATABASE_URL |
No | — | Postgres (for tileset rows) |
S3_ENDPOINT |
Yes | — | S3-compatible endpoint URL |
S3_BUCKET |
Yes | — | S3 bucket name |
S3_ACCESS_KEY |
Yes | — | S3 access key |
S3_SECRET_KEY |
Yes | — | S3 secret key |
S3_REGION |
No | us-east-1 |
S3 region |
| Variable | Required | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_API_URL |
No | http://localhost:8080 |
Rust API URL (client-side) |
AUTH_SECRET |
No | — | NextAuth session encryption secret |
AUTH_GITHUB_ID |
No | — | GitHub OAuth app client ID |
AUTH_GITHUB_SECRET |
No | — | GitHub OAuth app client secret |
JWT_SECRET |
No | — | Shared HS256 secret for API JWTs |
DATABASE_URL |
No | — | Postgres (for Auth.js user store) |
For authentication troubleshooting and the current improvement backlog, see the repository review.
Interactive API documentation is available via Swagger UI at /swagger-ui when the API is running. The OpenAPI spec is served at /api-docs/openapi.json.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
No | Health check |
| POST | /api/tiles |
Optional | Process image into tiles |
| GET | /api/tiles/{id}/progress |
No | SSE progress stream |
| GET | /api/tiles/{id}/download |
Required | Download tiles as ZIP (presigned redirect) |
| GET | /api/tiles/{id}/download/pmtiles |
Required | Download tiles as PMTiles (presigned redirect) |
| GET | /api/tiles/{id}/thumbnail |
No | Tileset thumbnail (JPEG) |
| GET | /api/tilesets |
Optional | List tilesets (paginated) |
| POST | /api/tilesets |
Required | Create tileset |
| GET | /api/tilesets/{slug} |
Optional | Get tileset details |
| PATCH | /api/tilesets/{slug} |
Required | Update tileset |
| DELETE | /api/tilesets/{slug} |
Required | Delete tileset + S3 cleanup |
| GET | /api/tilesets/{slug}/pmtiles-url |
Optional | Presigned PMTiles URL for range requests |
| GET | /api/user |
Required | Current user info + storage usage |
| POST | /api/user/deactivate |
Required | Deactivate account |
| POST | /api/user/reactivate |
Required | Reactivate within 30-day grace period |
| POST | /api/admin/purge-deactivated |
Admin | Purge accounts deactivated > 30 days |
| GET | /api/notifications |
Required | List notifications |
| POST | /api/notifications |
Required | Create notification |
| POST | /api/notifications/read |
Required | Mark all notifications as read |
| DELETE | /api/notifications |
Required | Clear all notifications |
| POST | /api/keys |
Required | Create API key (Pro only) |
| GET | /api/keys |
Required | List API keys and CLI devices |
| DELETE | /api/keys |
Required | Revoke all API keys |
| POST | /api/keys/cli |
Required | Authorize a CLI device |
| DELETE | /api/keys/self |
API key | Revoke the calling credential |
| PATCH | /api/keys/{key_id} |
Required | Rename an API key or CLI device |
| DELETE | /api/keys/{key_id} |
Required | Revoke one API key or CLI device |
| GET/POST | /api/projects |
Required | List or create projects |
| PATCH/DELETE | /api/projects/{project_id} |
Required | Update or delete a project |
| GET | /api/jobs |
Required | List persistent processing jobs |
| GET | /api/jobs/{job_id} |
Required | Get processing job state |
| POST | /api/jobs/{job_id}/retry |
Required | Retry a failed job |
| POST | /api/jobs/{job_id}/cancel |
Required | Cancel a queued or active job |
tileforge tiles <IMAGE> [OPTIONS]| Flag | Default | Description |
|---|---|---|
-o, --output <PATH> |
tiles.zip |
Output ZIP file path |
-t, --tile-size <PX> |
256 |
Tile size in pixels |
--min-zoom <N> |
0 |
Minimum zoom level |
--max-zoom <N> |
auto | Maximum zoom level (calculated from image dimensions) |
--projection <TYPE> |
flat |
flat or mercator |
--format <FORMAT> |
png |
png, jpeg, or webp tile encoding |
--quality <1-100> |
85 |
JPEG quality; ignored for PNG and lossless WebP |
--streaming |
Force streaming mode (lower memory) | |
--naive |
Force naive mode (faster for small images) |
# Basic usage — auto-selects processing strategy
tileforge tiles world_map.png -o tiles.zip
# Mercator projection for an equirectangular world map
tileforge tiles equirect_world.png -o mercator_tiles.zip --projection mercator
# Custom tile size and zoom range
tileforge tiles large_image.jpg -o tiles.zip --tile-size 512 --max-zoom 6
# Force streaming mode for a huge PNG
tileforge tiles huge.png -o tiles.zip --streaming
# Authenticate this device through the browser, then use hosted jobs
tileforge auth login
tileforge auth status
tileforge cloud submit globe.tif --projection mercator --wait
tileforge cloud jobstiles.zip
├── 0/0/0.png # Zoom 0: 1 tile
├── 1/0/0.png # Zoom 1: 4 tiles
├── 1/0/1.png
├── 1/1/0.png
├── 1/1/1.png
├── 2/0/0.png # Zoom 2: 16 tiles
├── ...
Compatible with Leaflet, Mapbox GL JS, OpenLayers, MapLibre, Google Maps, and any library that supports XYZ tile URLs.
- Drop or browse for an image (PNG, JPEG, WebP, TIFF, or GeoTIFF)
- Configure tile size, max zoom level, and projection
- Process — choose private Local mode or Pro Server mode
- Preview — interactive Leaflet map rendered from the in-memory tiles
- Download — single ZIP file ready to deploy
The web UI shows real-time progress (zoom level, tile count, percentage) and post-processing stats (duration, tile count, peak memory estimate).
Standard equirectangular mapping. Each zoom level uniformly scales the source image to fit the tile grid. Suitable for:
- Fantasy/game maps
- Floor plans and architectural drawings
- Artwork and illustrations
- Any non-geographic image
Web Mercator (EPSG:3857) Y-axis remapping. Assumes the source image is an equirectangular (plate carree) projection spanning approximately ±85.051° latitude. Tiles are warped so that equal-size tile rows cover equal Mercator-projected latitude bands. Suitable for:
- Real-world geographic maps
- Satellite imagery in equirectangular format
- Any source intended for use with standard web mapping libraries
The X axis is unchanged between projections — both are linear in longitude.
Naive (small images): Decode the full image, resize to each zoom level's canvas size, crop tiles from the canvas. Simple and fast for images under ~256 MB decoded.
Streaming PNG (large PNGs): Decode the PNG row-by-row using the png crate. For each tile row at max zoom, decode only the source rows needed, extract tiles, then release the rows. Lower zoom levels are built by merging tiles (4 → 1) using a pyramid builder. Peak memory is roughly one tile row's worth of source data.
Strip extraction (large non-PNGs): Fully decode the image (unavoidable for JPEG), but never create resized copies. Each tile is individually cropped and resampled from the source. Pyramid builder handles lower zoom levels.
Instead of creating full-canvas images at every zoom level, the pyramid builder merges tiles bottom-up:
- Generate all tiles at max zoom
- Every pair of tile rows triggers a merge: 4 tiles (2x2) are composited into a 2x tile and downscaled to 1 tile at zoom-1
- Merges cascade recursively down to zoom 0
This means lower zoom levels are generated with no additional source image access.
GitHub Actions runs Rust tests, Clippy, a WASM bridge check, the Next.js production build, and auth policy tests for pushes and pull requests. The production web/Dockerfile builds the release WASM bundle before compiling the Next.js application; generated artifacts are not committed.
| Component | Technology |
|---|---|
| Core library | Rust (image, png, zip, pmtiles, thiserror) |
| HTTP API | Rust + axum, tower-http |
| Worker | Rust + async-nats, redis, rust-s3, sqlx |
| WASM bridge | wasm-bindgen, js-sys, wasm-pack |
| CLI | Rust + clap |
| Web framework | Next.js 16 (App Router, Turbopack) |
| Auth | Auth.js v5 (GitHub OAuth), HS256 JWT |
| Styling | Tailwind CSS v4 + shadcn/ui |
| Map preview | Leaflet + react-leaflet + pmtiles |
| ZIP (browser) | fflate |
| Database | PostgreSQL + sqlx migrations |
| Queue | NATS JetStream (durable job queue with retry/DLQ; Redis BRPOP fallback) |
| Storage | S3-compatible (Railway bucket / MinIO) |
| CI | GitHub Actions |
Made with ♥ by sandybridge