How Project N.O.M.A.D. Provides Offline Maps Using Protomaps: Architecture and Implementation
Project N.O.M.A.D. delivers fully offline mapping by downloading Protomaps Basemaps assets and PMTiles files to local storage, serving them via a custom AdonisJS static provider, and generating dynamic MapLibre style JSON that points to local pmtiles:// URIs.
Project N.O.M.A.D. (Nomad Offline Maps and Data) implements a self-contained offline map stack that eliminates external network dependencies for critical geospatial functionality. By combining Protomaps Basemaps with the PMTiles archive format, the system stores vector tiles and styling assets on the local filesystem, ensuring maps remain accessible in air-gapped or connectivity-challenged environments. This architecture enables users to browse detailed maps without ever sending requests to external tile servers.
The Four-Component Architecture
The offline map system in Crosstalk-Solutions/project-nomad consists of four integrated parts that work together to replace cloud-based map providers with a local-first solution:
-
Base Assets: Static styling resources including
nomad-base-styles.json, sprites, and fonts extracted from the official Protomaps basemap project. These are fetched once from theproject-nomad-mapsrepository as abase-assets.tar.gztarball and stored instorage/maps. -
Region PMTiles: Vector tile containers for specific geographic regions (e.g.,
alaska_2025-12.pmtiles) that follow the PMTiles specification for single-file archive access. These reside instorage/maps/pmtiles. -
Static File Server: A custom AdonisJS provider at
admin/providers/map_static_provider.tsthat registers middleware to expose thestorage/mapsdirectory via HTTP, enabling the MapLibre client to request assets using standard URLs. -
Dynamic Style Generation: Runtime synthesis of MapLibre style JSON documents via
MapService.generateStylesJSON, which merges the base style template with discovered local PMTiles files and rewrites source URLs to use thepmtiles://scheme pointing at the local server.
Fetching Base-Map Assets
When the service initializes or a user requests a new region, MapService.ensureBaseAssets() verifies the presence of required styling resources. If the cache flag baseAssetsExistCache indicates missing assets, the system executes downloadBaseAssets() to retrieve the foundational map components:
const defaultTarFileURL = new URL(
this.baseAssetsTarFile,
'https://github.com/Crosstalk-Solutions/project-nomad-maps/raw/refs/heads/master/'
);
await doResumableDownloadWithRetry({ url: defaultTarFileURL.toString(), ... });
await extract({ cwd: join(process.cwd(), this.mapStoragePath), file: tempTarPath, strip: 1 });
The downloaded tarball contains three critical components: the nomad-base-styles.json MapLibre style template, sprite atlases for map icons, and glyph ranges for font rendering. These static assets support every offline region and require only a single download per deployment.
Managing Region PMTiles Downloads
Regional map data arrives as individual PMTiles files downloaded on demand through MapService.downloadCollection(). This method reads the manifest at collections/maps.json via CollectionManifestService, filters out already-installed resources by checking the InstalledResource model, and dispatches background jobs for each new file:
await RunDownloadJob.dispatch({
url: resource.url,
filepath: join(process.cwd(), this.mapStoragePath, 'pmtiles', filename),
allowedMimeTypes: PMTILES_MIME_TYPES,
filetype: 'map',
resourceMetadata: { resource_id: resource.id, version: resource.version, collection_ref: slug },
});
Upon completion, the downloadRemoteSuccessCallback creates an InstalledResource database entry recording the file size, version, and filesystem path. This tracking enables the system to enumerate available offline regions and avoid redundant downloads during subsequent update checks.
Serving Assets via Custom Static Middleware
AdonisJS typically serves static files exclusively from the /public directory. Project N.O.M.A.D. circumvents this limitation by registering MapStaticProvider, which instantiates a singleton middleware pointing at the storage/maps directory:
const path = join(process.cwd(), '/storage/maps');
this.app.container.singleton(MapsStaticMiddleware, () => new MapsStaticMiddleware(path, config));
This configuration exposes two critical URL patterns: /maps/basemaps-assets/ for sprites and fonts, and /maps/pmtiles/ for vector tile archives. When the MapLibre client requests http://localhost:3333/maps/pmtiles/alaska_2025-12.pmtiles, the middleware streams the file directly from the local filesystem without network latency.
Generating Dynamic Style JSON
The bridge between the local file server and the rendering client occurs in MapService.generateStylesJSON(host). This method constructs a valid MapLibre style document that redirects all tile requests to the local PMTiles files rather than remote endpoints:
- Validates base assets exist via
checkBaseAssetsExist() - Loads the
nomad-base-styles.jsontemplate - Enumerates available regions using
listRegions()to scanstorage/maps/pmtiles - Constructs a
sourcesarray where each entry specifies:
{
"type": "vector",
"attribution": "<a href=\"https://github.com/protomaps/basemaps\">Protomaps</a> © <a href=\"https://openstreetmap.org\">OpenStreetMap</a>",
"url": "pmtiles://http://<host>/maps/pmtiles/alaska_2025-12.pmtiles"
}
- Rewrites
spriteandglyphsURLs to reference the local static server (e.g.,http://<host>/maps/basemaps-assets/sprites/v4/light) - Returns the merged JSON to the client
The pmtiles:// protocol handler in MapLibre GL JS interprets these URLs to fetch vector tiles via HTTP range requests against the local PMTiles files, enabling efficient random access to the archived tiles without extracting the entire archive.
Complete Offline Workflow
The end-to-end process for providing offline maps follows this sequence:
- Bootstrap:
ensureBaseAssets()downloads and extracts the base tarball containing Protomaps styling resources tostorage/maps - Acquisition:
downloadCollection()ordownloadRemote()fetches specific region PMTiles files tostorage/maps/pmtilesbased on user selection - Serving:
MapStaticProviderregisters middleware to handle HTTP requests for/maps/*routes, serving both assets and tile archives from the local filesystem - Configuration: Client requests to
GET /api/maps/style.jsontriggergenerateStylesJSON(), which assembles a style document pointing all sources at localpmtiles://URLs - Rendering: The MapLibre/React-Map-GL instance consumes the style JSON and renders the map using exclusively local resources, functioning without internet connectivity
Implementation Examples
Initialize Offline Map Infrastructure
Bootstrap the system by ensuring base assets are present before serving client requests:
import { MapService } from '#services/map_service';
async function bootstrapOfflineMaps() {
const mapService = new MapService();
const ok = await mapService.ensureBaseAssets(); // pulls base-assets.tar.gz if missing
console.log('Base assets ready:', ok);
}
bootstrapOfflineMaps();
Enumerate Available Regions
Query the local storage to display which offline maps are currently available:
import { MapService } from '#services/map_service';
async function listRegions() {
const mapService = new MapService();
const { files } = await mapService.listRegions();
console.table(files.map(f => f.name));
}
listRegions();
Generate Client Style Configuration
Create a valid MapLibre style JSON for a specific host endpoint:
import { MapService } from '#services/map_service';
async function getStyle(host: string) {
const mapService = new MapService();
const style = await mapService.generateStylesJSON(host);
// send `style` back to the client (e.g. via an API endpoint)
console.log(JSON.stringify(style, null, 2));
}
getStyle('my.nomad.local');
Summary
- Project N.O.M.A.D. provides offline maps using Protomaps Basemaps and PMTiles archives stored in
storage/maps. - The
MapServiceclass inadmin/app/services/map_service.tshandles downloading base assets viadownloadBaseAssets()and region files viadownloadCollection(). - A custom AdonisJS provider at
admin/providers/map_static_provider.tsserves local files at/maps/routes, bypassing the standard public directory limitation. - Dynamic style generation creates MapLibre-compatible JSON with
pmtiles://URLs pointing to local archives, enabling true offline rendering without external dependencies. - The system tracks installed resources using the
InstalledResourcemodel to manage updates and prevent duplicate downloads.
Frequently Asked Questions
What is PMTiles and why does Project N.O.M.A.D. use it?
PMTiles is a single-file archive format for storing vector tiles that supports HTTP range requests for efficient random access. Project N.O.M.A.D. uses PMTiles because it allows storing entire regional maps (millions of tiles) in one file without extraction, reducing filesystem overhead while maintaining fast tile retrieval through byte-range requests.
How does the system handle map updates when new versions become available?
The downloadCollection() method checks the collections/maps.json manifest against existing InstalledResource database records. When a newer version appears in the manifest, the system dispatches a new RunDownloadJob to fetch the updated PMTiles file, and the generateStylesJSON method automatically includes the new file in subsequent style responses.
Can the maps function completely without internet after initial setup?
Yes. Once ensureBaseAssets() has downloaded the base styling tarball and the required region PMTiles files are stored in storage/maps/pmtiles, the MapStaticProvider serves all resources locally. The generated style JSON uses pmtiles:// URLs referencing the local server, ensuring the MapLibre client never attempts external network requests.
Where are the offline map files physically stored?
All assets reside in the storage/maps directory within the application root. Base assets (styles, sprites, fonts) extract to storage/maps/ directly, while regional vector tiles download to storage/maps/pmtiles/. The Docker configuration in install/sidecar-disk-collector/Dockerfile ensures this path persists across container restarts via volume mounts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →