Four upstream Python services (TiTiler, TiTiler-pgSTAC, STAC, tipg) are launched either by Node child_process or Compose profile, then fronted by a single proxy that gives the browser one origin and admin-gates anything mutating.
The browser never talks to ports 8881–8884 directly — if a STAC or tile request "works locally but fails in production," the answer is almost always the proxy block in adjacent-servers-proxy.js, not the upstream service.
MMGIS leans on a small constellation of best-of-breed Python services for things Node is bad at: tiling cloud-optimized GeoTIFFs, serving a STAC catalog, and producing vector tiles from . Rather than reimplement any of it, MMGIS runs each as a separate process and proxies through to it. Everything in this component lives under adjacent-servers/.
The four services
- TiTiler (
titiler/, port8883) — DevelopmentSeed's dynamic raster tile server. Reads COGs (cloud-optimized GeoTIFFs) and emits map tiles on demand. The script entry isuvicorn titiler.application.main:app. - TiTiler-pgSTAC (
titiler-pgstac/, port8884) — TiTiler variant that mosaics imagery driven by a STAC search in Postgres. Pairs withstac-fastapiand shares themmgis-stacdatabase. - STAC (
stac/, port8881) —stac-fastapi-pgstac, a SpatioTemporal Asset Catalog server backed by the pgstac Postgres extension. The MMGIS Stac feature module talks to this to register and search catalog items. - tipg (
tipg/, port8882) — DevelopmentSeed's OGC API Features / vector tile server. Serves any PostGIS table or function as features or MVT tiles.
Each folder is intentionally tiny: an .env.example, plus start-<service>.sh / .bat scripts that just python -m dotenv run python -m uvicorn <app> --port $1. There is no MMGIS-authored Python here — the upstream packages do the work.
Two ways to run them
The same set of services can be launched two ways, depending on deployment style:
- Local Node spawn.
adjacent-servers/adjacent-servers.jsis called from the main server bootstrap (see server-bootstrap). It iterates the four services and, for each one whoseWITH_<SERVICE>env flag is"true",child_process.spawns the matching start script. Useful fornpm startdevelopment outside Docker. - Docker Compose profiles. In
docker-compose.ymlanddocker-compose.dev.yml,stac-fastapi,tipg,titiler, andtitiler-pgstacall carryprofiles: ["stac"]. They only come up when you run with--profile stac, e.g.docker-compose --profile stac up.veloserveruses the same pattern under its own profile.
Either way, the services are opt-in. A bare docker-compose up runs only the MMGIS app and Postgres.
The proxy front door
The browser never talks to ports 8881–8884 directly. adjacent-servers-proxy.js is mounted by the Express bootstrap and registers one http-proxy-middleware route per enabled service:
//// STAC
if (process.env.WITH_STAC === "true") {
const stacTarget = `http://${isDocker ? "stac-fastapi" : "localhost"}:${
process.env.STAC_PORT || 8881
}`;
app.use(`${ROOT_PATH}/stac`, ensureAdmin(false, false, true),
createProxyMiddleware({ target: stacTarget, ... }));
}
A few things this earns:
- Single origin. Clients hit
/stac,/tipg,/titiler,/titilerpgstacon the MMGIS host; same cookies, same TLS, no CORS. - Auth gating. Every proxy is wrapped in
ensureAdmin(false, false, true)— anonymous GETs pass through, anything mutating requires admin auth. TiTiler also exempts/cog/stacfrom the read-only allowlist. - Docker vs. local routing. The
isDockerflag swapslocalhostfor the Compose service name (stac-fastapi,tipg, etc.). - Swagger fixup.
createSwaggerInterceptorrewrites the upstream service's OpenAPI doc on the fly so the embedded Swagger UI knows it's living under/<service>and not at root.
The same file also handles two extras: a dev-only generic /corsproxy for any external https:// URL, and a small "custom adjacent server" registry that lets operators wire arbitrary side services in via ADJACENT_SERVER_CUSTOM_<N>=["true","route","service","port"] env vars without code changes.
The WITH_<SERVICE> convention
Adding a service is a four-touch change: a WITH_FOO=true in .env, a FOO_PORT, a Compose entry (or a row in adjacent-servers.js), and a proxy block in adjacent-servers-proxy.js. Removing one is one env flag flip. That symmetry is the whole reason this directory exists as its own concept.
The Postgres spatial extension MMGIS depends on. Installed by init-db.js via
CREATE EXTENSION IF NOT EXISTS postgis. Backs every Geodataset, Draw user-feature,
and spatial query.
Related
- Backend (API server) — Server bootstrap and middlewareHow the MMGIS process starts, composes Express, mounts feature modules via the setup lifecycle, and attaches the WebSocket server.
- Backend (API server) — Feature modules under API/BackendThe repeating shape every backend feature follows, and a tour of the modules that matter (Datasets, Geodatasets, Draw, Config, Stac, Webhooks, Shortener, etc.).