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 the project-nomad-maps repository as a base-assets.tar.gz tarball and stored in storage/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 in storage/maps/pmtiles.

  • Static File Server: A custom AdonisJS provider at admin/providers/map_static_provider.ts that registers middleware to expose the storage/maps directory 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 the pmtiles:// 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:

  1. Validates base assets exist via checkBaseAssetsExist()
  2. Loads the nomad-base-styles.json template
  3. Enumerates available regions using listRegions() to scan storage/maps/pmtiles
  4. Constructs a sources array 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"
}
  1. Rewrites sprite and glyphs URLs to reference the local static server (e.g., http://<host>/maps/basemaps-assets/sprites/v4/light)
  2. 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:

  1. Bootstrap: ensureBaseAssets() downloads and extracts the base tarball containing Protomaps styling resources to storage/maps
  2. Acquisition: downloadCollection() or downloadRemote() fetches specific region PMTiles files to storage/maps/pmtiles based on user selection
  3. Serving: MapStaticProvider registers middleware to handle HTTP requests for /maps/* routes, serving both assets and tile archives from the local filesystem
  4. Configuration: Client requests to GET /api/maps/style.json trigger generateStylesJSON(), which assembles a style document pointing all sources at local pmtiles:// URLs
  5. 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 MapService class in admin/app/services/map_service.ts handles downloading base assets via downloadBaseAssets() and region files via downloadCollection().
  • A custom AdonisJS provider at admin/providers/map_static_provider.ts serves 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 InstalledResource model 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →