# How Project N.O.M.A.D. Provides Offline Maps Using Protomaps: Architecture and Implementation

> Discover how Project N.O.M.A.D. uses Protomaps for offline maps. Learn about its architecture for downloading basemaps and serving dynamic styles locally.

- Repository: [Crosstalk Solutions/project-nomad](https://github.com/Crosstalk-Solutions/project-nomad)
- Tags: architecture
- Published: 2026-03-16

---

**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`](https://github.com/Crosstalk-Solutions/project-nomad/blob/main/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`](https://github.com/Crosstalk-Solutions/project-nomad/blob/main/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:

```typescript
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`](https://github.com/Crosstalk-Solutions/project-nomad/blob/main/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`](https://github.com/Crosstalk-Solutions/project-nomad/blob/main/collections/maps.json) via `CollectionManifestService`, filters out already-installed resources by checking the `InstalledResource` model, and dispatches background jobs for each new file:

```typescript
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:

```typescript
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`](https://github.com/Crosstalk-Solutions/project-nomad/blob/main/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:

```json
{
  "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"
}

```

5. Rewrites `sprite` and `glyphs` URLs to reference the local static server (e.g., `http://<host>/maps/basemaps-assets/sprites/v4/light`)
6. 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:

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/Crosstalk-Solutions/project-nomad/blob/main/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`](https://github.com/Crosstalk-Solutions/project-nomad/blob/main/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`](https://github.com/Crosstalk-Solutions/project-nomad/blob/main/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.