# Configuration

Environment variables read by the server and tools. Variables marked **secret** hold credentials: never log them or put them in URLs. Empty values count as unset.

## Server

| Variable | Default | Secret | Description |
|---|---|---|---|
| `PORT` | `8080` |  | HTTP port of the API. |
| `HOST` | `0.0.0.0` |  | Interface to listen on. |
| `LOG_LEVEL` | `info` |  | Log level (fatal, error, warn, info, debug, trace). |
| `WHEREBOX_PUBLIC_URL` | `derived from the request` |  | Public base URL used for `url` in JSON responses (e.g. `https://maps.example.com`). |
| `WHEREBOX_RENDER_CONCURRENCY` | `CPUs − 1` |  | Renders running at the same time per process. |
| `WHEREBOX_MAP_REUSE` | `300` |  | Renders per pooled MapLibre instance before it is recycled (bounds native memory). |
| `WHEREBOX_METRICS_TOKEN` | `(metrics disabled)` | secret | Bearer token for the Prometheus endpoint `/metrics`; unset keeps it disabled. Scrape with `Authorization: Bearer <token>`. |
| `WHEREBOX_RENDER_QUEUE` | `64` |  | Renders waiting beyond the concurrency limit; more get 503 with Retry-After. |
| `WHEREBOX_SITE_URL` | — |  | Base URL of the website that hosts the playground page (e.g. `https://wherebox.dev`). When set, `/playground` redirects to `<base>/playground/`, which must route `/v1/*` to this API on the same origin. Unset: this server serves its own standalone playground. |
| `WHEREBOX_TRUST_PROXY` | `(no proxy trusted)` |  | Reverse proxies allowed to set the client address through `X-Forwarded-For` (and the protocol and host through `X-Forwarded-Proto`/`-Host`): addresses or CIDRs separated by commas, e.g. `127.0.0.1` for Caddy on the same machine, or `loopback`. Unset, the socket peer is the client, and a spoofed `X-Forwarded-For` is ignored. Behind a proxy, set it: otherwise every visitor has the proxy's address and all per-IP limits (anonymous allowance, sign-in emails, OAuth client registration) become one shared bucket. Never list addresses that untrusted clients can connect from. |
| `WHEREBOX_DOCS_URL` | `https://wherebox.dev/docs` |  | Base URL of the published docs. Problem `type` URIs point at `<base>/errors#<code>`; set it to serve your own copy of the docs (the code after `#` stays the same). |

## Data

| Variable | Default | Secret | Description |
|---|---|---|---|
| `DATABASE_URL` | `postgres:///wherebox` | secret | PostGIS database with the imported areas, places and countries. Country-level areas fall back to Natural Earth (the countries table) when the OSM import does not cover them. |

## Cache

| Variable | Default | Secret | Description |
|---|---|---|---|
| `WHEREBOX_GEOMETRY_CACHE` | `256` |  | Area/subdivision geometries kept in memory (LRU; 0 = off). Cleared when the data version changes. |
| `WHEREBOX_CACHE` | `disk` |  | Rendered-image cache: `disk`, `s3` (S3/R2/MinIO) or `none`. |
| `WHEREBOX_IMAGE_CACHE_DIR` | `data/cache/images` |  | Directory for the disk image cache. |
| `WHEREBOX_IMAGE_CACHE_MAX_MB` | `2048` |  | Disk image cache cap; least-recently-used images are evicted (0 = unlimited). |
| `WHEREBOX_TILE_TTL_HOURS` | `720` |  | After this many hours cached tiles, glyphs and terrain are revalidated in the background (stale ones keep serving). |
| `WHEREBOX_STYLE_TTL_HOURS` | `24` |  | Revalidation age for cached styles, TileJSON and sprites, so basemap updates show up. |
| `WHEREBOX_CACHE_DIR` | `data/cache/http` |  | Cache for basemap styles, tiles, glyphs and terrain. |
| `WHEREBOX_S3_BUCKET` | — |  | Bucket for `WHEREBOX_CACHE=s3` (credentials via the standard AWS variables). |
| `WHEREBOX_S3_PREFIX` | `maps/` |  | Key prefix inside the bucket. |
| `WHEREBOX_S3_ENDPOINT` | `AWS` |  | Endpoint for S3-compatible stores (R2, MinIO). |
| `WHEREBOX_S3_REGION` | `auto` |  | Region for the S3 client. |

## Routing

| Variable | Default | Secret | Description |
|---|---|---|---|
| `WHEREBOX_ROUTING_PROVIDER` | `valhalla` |  | Default routing provider: `valhalla`, `openrouteservice` or `graphhopper`. |
| `WHEREBOX_VALHALLA_URL` | `(routing disabled)` |  | Valhalla server for car/bike/foot routes. Self-host it for production; the public FOSSGIS instance is for development only. |
| `WHEREBOX_ORS_KEY` | — | secret | Server-side openrouteservice key (callers can send `X-Routing-Key` instead). |
| `WHEREBOX_ORS_URL` | `https://api.heigit.org/openrouteservice` |  | openrouteservice base URL (HeiGIT's hosted API; the old `api.openrouteservice.org` was shut off in Nov 2026). For a self-hosted openrouteservice, its base URL without `/v2`. |
| `WHEREBOX_GRAPHHOPPER_KEY` | — | secret | Server-side GraphHopper key. |
| `WHEREBOX_GRAPHHOPPER_URL` | `https://graphhopper.com/api/1` |  | GraphHopper base URL. |
| `WHEREBOX_ROUTING_TIMEOUT_MS` | `10000` |  | Timeout for routing requests. |

## Geocoding

| Variable | Default | Secret | Description |
|---|---|---|---|
| `WHEREBOX_GEOCODER_URL` | `(fallback disabled)` |  | Geocoder for place names the imported OSM data does not know (`place:` points and route ends). Query parameters in the URL (e.g. `?key=…`) are kept on every request and never appear in errors. Self-host Nominatim or Photon for production; the public instances allow about 1 request/s and are for development only. |
| `WHEREBOX_GEOCODER_PROVIDER` | `nominatim` |  | API flavour of `WHEREBOX_GEOCODER_URL`: `nominatim` (also LocationIQ and other Nominatim-compatible services) or `photon`. |
| `WHEREBOX_GEOCODER_USER_AGENT` | `wherebox (self-hosted static map renderer)` |  | User-Agent sent to the geocoder. Public Nominatim requires one that identifies your application. |
| `WHEREBOX_GEOCODER_RPS` | `1` |  | Maximum geocoder requests per second per process (0 = unlimited). Results are cached, so repeated names cost nothing. |
| `WHEREBOX_GEOCODER_TIMEOUT_MS` | `5000` |  | Timeout per lookup, including time spent waiting for a rate-limit slot. |

## Remote assets (security)

| Variable | Default | Secret | Description |
|---|---|---|---|
| `WHEREBOX_AUTH` | `none` |  | API access: `none` (open, for self-hosting) or `keys` (API keys, signed URLs, rate limits, quotas, usage metering). |
| `WHEREBOX_AUTH_SECRET` | `(required with keys)` | secret | Master secret (≥ 32 chars) for key hashing and URL-signing secrets. Changing it invalidates all keys. |
| `WHEREBOX_PLANS` | `built-in free/plus/internal` |  | JSON plan definitions replacing the built-in ones: `{"free": {"monthlyRenders": 10000, "monthlyRoutedRenders": 1000, "perMinute": 60, "maxPixels": 4194304, "routing": true}, …}`. Limits apply per account; 0 means unlimited. New accounts get `free`. The `anonymous` plan (built-in unless given here) sets the demo allowance for requests without a key, per client network, with `dailyRenders` (new renders per UTC day) instead of a monthly quota. |
| `WHEREBOX_ANONYMOUS` | `on` |  | With `WHEREBOX_AUTH=keys`: `on` gives requests without any key a small demo allowance (GET maps and searches only, limits from the `anonymous` plan, per IPv4 address or IPv6 /64, counted in memory and reset at midnight UTC), so a public playground and docs work without signing up; `off` makes every request need a key. A malformed or unknown key is always refused, never treated as anonymous. Set `WHEREBOX_TRUST_PROXY` behind a reverse proxy, or all visitors share one allowance. |
| `WHEREBOX_ANONYMOUS_DAILY_CAP` | `2000` |  | New renders per UTC day for all anonymous clients together (0 = no cap), so many addresses cannot turn the demo allowance into free bulk rendering. Cache hits do not count. |
| `WHEREBOX_MORE_USAGE_URL` | `(none)` |  | Page explaining how to ask for higher limits, linked from quota and plan-limit errors (the hosted service uses its fair-use page, `https://wherebox.dev/docs/guides/fair-use/#need-more`). |
| `WHEREBOX_CONTACT_EMAIL` | `(none)` |  | Address for limit requests (hosted: `hello@wherebox.dev`). The Usage page opens an email to it with the account details filled in; without it, the page links to `WHEREBOX_MORE_USAGE_URL`. |
| `WHEREBOX_GITHUB_CLIENT_ID` | `(GitHub sign-in off)` |  | Client ID of the GitHub OAuth app for "Continue with GitHub" in the console (`WHEREBOX_AUTH=keys`). Its callback URL is `<WHEREBOX_PUBLIC_URL>/auth/github/callback`. |
| `WHEREBOX_GITHUB_CLIENT_SECRET` | `(GitHub sign-in off)` | secret | Client secret of that GitHub OAuth app; set both or neither. |
| `WHEREBOX_RESEND_API_KEY` | `(links printed to stdout)` | secret | Resend API key for magic-link sign-in emails. Unset, sign-in links are printed to the log, which is allowed only when the public URL is http://localhost. |
| `WHEREBOX_MAIL_FROM` | — |  | Sender of sign-in emails, e.g. `Wherebox <login@wherebox.dev>` (a domain verified in Resend). Required with `WHEREBOX_RESEND_API_KEY`. |
| `WHEREBOX_STRIPE_SECRET_KEY` | `(billing off)` | secret | Stripe secret (or restricted) API key for the console billing pages. Billing is on only when this, `WHEREBOX_STRIPE_WEBHOOK_SECRET` and `WHEREBOX_STRIPE_PRICES` are all set. |
| `WHEREBOX_STRIPE_WEBHOOK_SECRET` | `(billing off)` | secret | Signing secret (`whsec_…`) of the Stripe webhook endpoint `<WHEREBOX_PUBLIC_URL>/webhooks/stripe`; every webhook is verified with it. |
| `WHEREBOX_STRIPE_PRICES` | `(billing off)` |  | JSON mapping plan id to Stripe price id, e.g. `{"plus":"price_…"}`. Each plan must exist in the plans config (`WHEREBOX_PLANS` or the built-in ones). |
| `WHEREBOX_ICON_HOSTS` | `(remote icons disabled)` |  | Comma-separated hosts allowed for remote marker icons (`*.example.com` matches subdomains). Empty = `data:` URIs only. |
| `WHEREBOX_ICON_MAX_BYTES` | `100000` |  | Maximum icon size. |
| `WHEREBOX_ICON_TIMEOUT_MS` | `5000` |  | Timeout for fetching a remote icon. |
| `WHEREBOX_FONT_HOSTS` | `same as WHEREBOX_ICON_HOSTS` |  | Hosts allowed for custom font URLs. |
| `WHEREBOX_FONT_MAX_BYTES` | `3000000` |  | Maximum custom font size. |
| `WHEREBOX_FONT_TIMEOUT_MS` | `8000` |  | Timeout for fetching a custom font. |

## Import tools

| Variable | Default | Secret | Description |
|---|---|---|---|
| `WORK_DIR` | `data/work` |  | Scratch directory for the OSM and Natural Earth imports. |
| `NE_WORLDVIEWS` | `all` |  | Natural Earth worldviews imported by `pnpm import:countries` for the `worldview` parameter: `all`, `none`, or codes such as `in cn ma` (about 13 MB download each). Border lines for all worldviews are always imported; this picks the country shapes. |
| `NE_VERSION` | `v5.1.2` |  | Natural Earth release used by `pnpm import:countries` (pinned for reproducible renders). |

## Deployment

| Variable | Default | Secret | Description |
|---|---|---|---|
| `WHEREBOX_DOMAIN` | `localhost` |  | Public domain for Caddy (TLS) in docker-compose. |

## Debugging

| Variable | Default | Secret | Description |
|---|---|---|---|
| `WHEREBOX_DEBUG` | — |  | Set to log MapLibre Native errors. |

## Local MCP server (`npx @wherebox/mcp`)

| Variable | Default | Secret | Description |
|---|---|---|---|
| `WHEREBOX_API_URL` | `https://api.wherebox.dev` |  | Wherebox API the local MCP server calls: the hosted service, or your own server (e.g. `http://localhost:8080`). |
| `WHEREBOX_API_KEY` | `(none: only for servers without keys)` | secret | Secret key (`sk_…`) the local MCP server sends as `Authorization: Bearer`. Required for the hosted service; leave unset for a self-hosted server with `WHEREBOX_AUTH=none`. |
