Documentation

Overview

A Corviont stack runs maps, routing, and search fully offline on your device. Once started, your application talks to a single local HTTP entrypoint - no external map APIs required.

The stack includes:

  • Vector tiles: served locally via the go-pmtiles server.
  • Routing API: an offline routing service based on Valhalla.
  • Geocoder API: lightweight forward and reverse geocoding backed by SQLite.
  • Optional UI: a MapLibre frontend wired to the local endpoints.

Requirements

OS & architecture

  • 64-bit OS is required.
  • Supported architectures: linux/amd64 and linux/arm64.
  • Verified on Ubuntu and Raspberry Pi OS (64-bit).

To check your architecture on Linux / Raspberry Pi:

uname -m 
# x86_64 or aarch64  => OK

getconf LONG_BIT
# 64 => OK 

Docker & Docker Compose

  • Docker installed and running, including Docker Compose.

Quick sanity check:

# Both should output a version
docker version
docker compose version

Getting started

Corviont ships as a self-contained pack (data + Docker Compose stack). Once you have a pack, you can run it locally on your machine or edge device in a few minutes.

Don't have a pack yet?
Request a pilot - we'll email you the Vienna offline eval pack so you can test on your own hardware.
Public sample: Monaco demo repo.
Prefer no install? Try the live Vienna demo.

1. Get the pack on disk

If you received a .zip/.tar.gz pack, extract it and cd into the extracted folder. If you're using the public sample repo, clone it and cd into the repo.

# Example (archive)
tar -xzf corviont-pack.tar.gz
cd corviont-pack

# Example (repo)
git clone https://github.com/corviont/monaco-demo.git
cd monaco-demo

2. Choose the port

Set the port you want the stack to listen on (3000 is the default used in this documentation). This creates a .env file that Docker Compose will pick up automatically:

echo "CORVIONT_PORT=3000" > .env

3. (Optional) Enable CORS

If you want to call Corviont APIs from a browser app running on a different origin (e.g. http://localhost:3001), append this to your .env:

# Supports a comma-separated allowlist (e.g. http://localhost:3001,http://localhost:5173) or "*" to allow any origin
echo "CORVIONT_CORS_ALLOWED_ORIGINS=http://localhost:3001" >> .env

4. Start the stack

Start all services in the background. On some Linux / Raspberry Pi setups you may need to prefix commands with sudo.

docker compose up -d

Once the stack starts, open http://localhost:3000/ (or whatever you chose for CORVIONT_PORT) in your browser. You should see:

  • A map rendered for the pack's region
  • Search results when typing (e.g. cafe)
  • A route line + turn-by-turn directions after clicking two points on map or searching in Routing mode

5. Stop the stack

To stop all services and free ports:

docker compose down

See the FAQ below for common issues around missing CORVIONT_PORT or Docker permissions.

Examples

A running stack exposes a single HTTP entrypoint, for example http://localhost:3000 (or whatever you chose for CORVIONT_PORT). All examples below assume that base URL.

Use case 1: Render a map (managed style)

Use this if you want the "it just works" path. Corviont provides a ready-to-use MapLibre style based on OpenMapTiles that already references the correct tiles and assets for the dataset version.

<html>

<head>
  <!--
	NOTE: For simplicity, this example uses unpkg for MapLibre.
    Corviont deployments bundle MapLibre (and all other assets) locally for fully offline use.
  -->
  <script src='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js'></script>
  <link href='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css' rel='stylesheet' />
  <style>
    html, body, #map { height: 100%; width: 100%; padding: 0;  margin: 0; }
  </style>
</head>

<body>
  <div id="map"></div>
  <script type="text/javascript">
    async function initMap() {
      // dataset.json provides the active dataset version (used for versioned asset URLs)
      const dataset = await (await fetch("/dataset.json")).json();

      new maplibregl.Map({
        container: "map",
        center: [dataset.ui_center.lon, dataset.ui_center.lat], // pack-defined UI center
        zoom: 15,
        transformRequest: (url) => ({ url: /^https?:\/\//i.test(url) ? url : absolutizeUrl(url) }),
      }).setStyle(`map-style/${dataset.version}.json`, {
        // make sprite/glyph URLs absolute (style contains relative paths)
        transformStyle: (_, next) => ({
          ...next,
          sprite: absolutizeUrl(next.sprite),
          glyphs: absolutizeUrl(next.glyphs),
        })
      });
    }
	
	// Corviont styles use relative URLs; MapLibre requires absolute URLs.
    // rewrite any relative request to an absolute one.
	function absolutizeUrl (url) {
		return [window.location.origin, window.location.pathname, url]
			.map(s => s.replace(/^\/+|\/+$/g, '')) // trim slashes
			.filter(Boolean) // remove empty parts 
			.join('/');
	}

    initMap().catch(console.error);
  </script>
</body>

</html>

Use case 2: Render a map (custom style)

Use this if you want a custom style (based on Corviont's default, another OpenMapTiles style, or something you build from scratch) wired directly to the raw vector tiles.

<html>

<head>
  <!--
	NOTE: For simplicity, this example uses unpkg for MapLibre.
    Corviont deployments bundle MapLibre (and all other assets) locally for fully offline use.
  -->
  <script src='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js'></script>
  <link href='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css' rel='stylesheet' />
  <style>
    html, body, #map { height: 100%; width: 100%; padding: 0;  margin: 0; }
  </style>
</head>

<body>
  <div id="map"></div>
  <script type="text/javascript">
    async function initMap() {
      const dataset = await (await fetch("/dataset.json")).json();

      new maplibregl.Map({
        container: "map",
        center: [dataset.ui_center.lon, dataset.ui_center.lat],
        zoom: 15,
        style: {
          version: 8,
          sources: {
            corviont: {
              type: "vector",
              tiles: [absolutizeUrl(`tiles/${dataset.version}/{z}/{x}/{y}.mvt`)]
            }
          },
          layers: [
            // minimal example, displays roads only
            { id: "roads", type: "line", source: "corviont", "source-layer": "transportation" }
          ]
        }
      });
    }

    initMap().catch(console.error);
  </script>
</body>

</html>

Use case 3: Routing

Use this to draw routes in your UI or power navigation features.

// replace with coordinates inside your pack's region
const from = { lat: 999, lon: 999 };
const to = { lat: 999, lon: 999 };

const input = {
	locations: [from, to],
	costing: "auto",
	directions_options: { units: "kilometers" },
	// Easiest to render in MapLibre: OSRM response + GeoJSON route geometry
	format: 'osrm',
	shape_format: "geojson"
};

const url = "/router/route?json=" + encodeURIComponent(JSON.stringify(input));
const output = await (await fetch(url)).json();
console.log(output);
/router proxies the Valhalla API (see the full Valhalla API overview). In the current Corviont stack, only /router/route is officially supported. Other Valhalla endpoints may be reachable but should be considered experimental.

Use case 4: Search

Use this for a search box (forward geocoding).

// default limit is 5, max is 10 
const query = "cafe";
const limit = 7;
const output = await (await fetch(`/geocoder/search?query=${encodeURIComponent(query)}&limit=${limit}`)).json();
console.log(output);

/*
Output: Array<GeocoderResult>

type GeocoderResult = {
  id: number;
  type: "poi" | "street" | "place";
  name: string;
  country: string; 
  city: string;
  street: string;
  housenumber: string;
  postcode: string;
  lat: number;
  lon: number;
};

*/

Use case 5: Reverse geocoding

Use this for "tap to get address" or "what's here?" interactions.

// default limit is 5, max is 10
// replace with coordinates inside your pack's region		
const lat = 999;
const lon = 999;

const limit = 10;
const radius_m = 100;

const output = await (await fetch(`/geocoder/reverse?lat=${lat}&lon=${lon}&limit=${limit}&radius_m=${radius_m}`)).json();
// output has the same shape as /geocoder/search
console.log(output);

Troubleshooting & FAQ

CORVIONT_PORT is missing

When running the stack, make sure you have a .env file in the root of the cloned repository and that it contains:

CORVIONT_PORT=3000

Then restart the stack:

docker compose down
docker compose up -d
I see no matching manifest for linux/arm/v7

Images are built only for 64-bit architectures (linux/amd64 and linux/arm64). If your system is 32-bit (for example armv7l), Docker cannot pull these images.

Check your architecture:

uname -m 
# x86_64 or aarch64  => OK

getconf LONG_BIT
# 64 => OK 
Docker says permission denied for /var/run/docker.sock

On some Linux / Raspberry Pi setups, Docker requires root access. You can either:

  1. Add your user to the docker group (recommended):
    sudo groupadd docker 2>/dev/null || true
    sudo usermod -aG docker "$USER"
    # Log out and log back in so the new group is applied
    groups   # should now include "docker"

    Then retry:

    docker compose up -d
  2. Or prefix commands with sudo:
    sudo docker compose up -d
Does the stack require internet access once it's running?

After the initial image and data download, Corviont runs fully offline: tiles, routing, and geocoding are all served from containers on your host or device. Your UI talks only to the local HTTP entrypoint.

Where can I find an example deployment?

Public sample: github.com/corviont/monaco-demo.

Recommended evaluation path: request a pilot to receive the Vienna offline eval pack by email.

Stores image for doc - does not display anywhere

Showcase image
Built on Unicorn Platform