# Migrating from Mapbox

Most of a Mapbox GL JS map moves across with the renderer swapped and the
URLs changed, because MapMap renders with MapLibre GL JS, which began as a
fork of Mapbox GL JS and still shares its style specification and most of
its API. `addSource`, `addLayer`, expressions, `queryRenderedFeatures` and
feature-state are the same calls with the same arguments. What changes is
narrow and knowable: the style URL, the `mapbox://` source URLs, the
newer Mapbox-only conveniences (slots, interactions, Standard style config)
and where your hosted tilesets live. This page is that list, gaps
included. Routing, geocoding and the other APIs are covered in
[migrating from Mapbox: what changes and what does not](/news/migrating-from-mapbox).

## Keys

A Mapbox `pk.` token becomes a MapMap `snk_` key, issued by one
unauthenticated call with no card:

```sh
curl -fsS -X POST "https://api.mapmap.ai/v1/keys" \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "accept_tos": true}'
```

`createMap` takes it as `apiKey`. For URL-only contexts (a style URL handed
to a native SDK) it travels as `?api_key=snk_…`.

## The map: style URL to `createMap`

```ts
// Before
import mapboxgl from "mapbox-gl";
mapboxgl.accessToken = "pk.…";
const map = new mapboxgl.Map({
  container: "map",
  style: "mapbox://styles/mapbox/streets-v12",
  center: [-0.1278, 51.5074],
  zoom: 12,
});

// After
import { createMap } from "@mapmap/maps";
import "maplibre-gl/dist/maplibre-gl.css";

const map = createMap({
  container: "map",
  apiKey: "snk_…",
  style: "light",            // "light" | "dark" | a Studio theme | a style URL
  center: [-0.1278, 51.5074],
  zoom: 12,
});
await map.whenReady();
```

```sh
npm install @mapmap/maps maplibre-gl pmtiles
```

`map.map` is the underlying MapLibre map. Everything you used to call on
your Mapbox `map` object you now call on `map.map`; the SDK's own helpers
(`RouteLayer`, `PlacesLayer`, `Geocoder` and the rest) sit beside it. If you
construct a `maplibregl.Map` yourself instead, import MapLibre as
`import * as maplibregl from "maplibre-gl"`: MapLibre 6 has no default
export.

The container needs an explicit height, exactly as before.

## Sources: `mapbox://` to `pmtiles://` or your own tiles

Every `mapbox://` URL in a style or an `addSource` call has to change,
because it resolves only against Mapbox's servers.

| Mapbox | MapMap |
|---|---|
| `mapbox://styles/mapbox/streets-v12` and the other Mapbox styles | `style: "light"` or `"dark"`, a [Studio](/studio) theme, or a hosted style URL from `POST /styles` |
| `mapbox://mapbox.mapbox-streets-v8` (the basemap vector tiles) | Comes with the style. To reference it yourself: `GET /tiles/{territory}/tiles.json`. The schema is OpenMapTiles, so layer and property names differ from Mapbox Streets |
| `mapbox://your-user.your-tileset` (an uploaded tileset) | A PMTiles file on your own bucket: `url: "pmtiles://https://your-bucket.example.com/your-data.pmtiles"`. See [Bring your own tiles](/docs/custom-data#bring-your-own-tiles-pmtiles-you-host) |
| `mapbox://mapbox.satellite` | Your own imagery as an XYZ `raster` source. MapMap does not offer a hosted satellite tileset for your apps |
| `mapbox://mapbox.mapbox-terrain-dem-v1` | `terrain: true` on `createMap`, which drapes the map over the same DEM the gateway answers `POST /elevation` from |
| GeoJSON, image, video and raster sources with `https` URLs | Unchanged |

Because the basemap schema is OpenMapTiles rather than Mapbox Streets, a
filter written against Streets property names (for example `class`
values or `maki` icons) needs checking against the
[styleable layer ids](/docs/maps#styleable-layer-ids). Your own data layers
are unaffected.

## `addLayer(..., beforeId)` and slots

Mapbox Standard puts your layers into named slots (`bottom`, `middle`,
`top`). MapLibre has no slots; you pass the id of the layer to insert
beneath, which is what Mapbox did before Standard and still accepts. The
MapMap styles share one layer skeleton, so the three slots map to three
fixed layer ids, listed in the
[insertion points table](/docs/custom-data#where-your-layer-goes-insertion-points):

```ts
// Mapbox Standard
map.addLayer({ id: "zones", type: "fill", source: "zones", slot: "middle" });

// MapMap today
map.map.addLayer({ id: "zones", type: "fill", source: "zones" }, "road-oneway");
```

Slots arrive in the SDK 0.17 under the same names: `map.slot("middle")`
returns the layer id to pass, resolved against the live style so a Studio
theme that drops a layer still works.

## `addInteraction` and featuresets

`map.addInteraction` and the Standard featuresets (`poi`, `place-labels`,
`buildings`) are Mapbox only. On MapLibre the same behaviour is the older
pattern, which Mapbox still supports too: layer-scoped events plus
feature-state.

```ts
// Mapbox
map.addInteraction("zone-click", {
  type: "click",
  target: { layerId: "zones" },
  handler: (e) => console.log(e.feature),
});

// MapMap today
map.map.on("click", "zones", (e) => console.log(e.features?.[0]));
```

The SDK adds an `addInteraction` helper in 0.17, shaped like Mapbox's, with
the `poi`, `place-labels` and `buildings` featureset ids mapped onto the
MapMap layers. Until then, [hover highlight with feature-state](/docs/examples/hover-highlight)
and [click a feature for a popup](/docs/examples/click-popup) are the
patterns to copy.

## What does not change

- **`queryRenderedFeatures` and `querySourceFeatures`**: same signatures,
  same results shape.
- **feature-state**: `setFeatureState`, `getFeatureState`,
  `removeFeatureState`, `promoteId` and the `["feature-state", …]`
  expression are the same. One difference to plan for: MapMap basemap
  features carry tile-assigned ids, not OpenStreetMap ids, so state on
  basemap layers is per tile. Your own sources behave as before.
- **Expressions and data-driven styling**, clustering with
  `clusterProperties`, heatmaps, fill-extrusion, `line-gradient` with
  `lineMetrics`, image and video sources, custom WebGL layers, terrain and
  the globe projection.
- **Popups and markers**: `new maplibregl.Popup()` and
  `new maplibregl.Marker()` replace their `mapboxgl` namesakes one for one.

The [examples gallery](/docs/examples) has fifteen runnable data-layer
samples written against exactly this surface.

## Standard style config to Studio themes and `global-state`

Mapbox Standard exposes runtime config (`setConfigProperty("basemap",
"lightPreset", "dusk")`, show or hide POI labels, theme colours). On
MapMap the design lives in a **Studio theme**: 19 palette slots, per-layer
overrides, POI and marker designs, saved as a theme document and
published as a hosted style. Switch between themes at run time with
`setStyle`, or let `ThemeScheduler` swap day and night on sun times.

For your own layers, MapLibre's `global-state` expression is the closest
thing to Mapbox's `config` expression: declare a value once, read it in
any paint or filter expression with `["global-state", "name"]`, and change
it with `map.map.setGlobalStateProperty("name", value)`.

## What MapLibre does not have

Stated plainly, so a gap does not surface halfway through a migration.
Raster-array sources and the raster particle layer, model layers
(`model` and 3D landmarks), the snow and rain layers, multiple lights and
style imports are not available, and we make no promise that they will
be. There is no `addInteraction` and no featuresets in MapLibre itself
(the SDK helper above covers the common case). Video sources are MapLibre
GL JS only: MapLibre Native on iOS and Android has none. Going the other
way, MapLibre has a `color-relief` layer for colouring elevation, which
Mapbox does not.

## Studio uploads to your own hosting

Mapbox Studio uploads a dataset, tiles it on Mapbox's servers and hosts
the tileset. MapMap has **no hosted tileset upload**, by decision: we do
not hold customer datasets. The route is bring your own hosting:

1. Tile on your own machine. The self-host distro ships
   `snfactory tile your-data.geojson -o your-data.pmtiles --layer your-data`;
   any PMTiles writer works.
2. Put the file on your own bucket (S3, R2, GCS, Azure) with range
   requests and CORS on.
3. Reference it as `pmtiles://https://…` in your style or an `addSource`
   call.

Small datasets need none of that: a GeoJSON URL in an `addSource` call is
fetched by the browser directly. Point datasets you want searchable (a
store finder, places along a route) go to `PUT /places`, up to 10,000 per
key. The full walk-through is on [add your own data](/docs/custom-data).

## Pricing

From [`/pricing.json`](/pricing.json), which is always the source of truth
over this page. Prices are in pounds sterling and prepaid: a key stops at
its limit rather than running up an invoice.

- **Free tier**: 50,000 calls a month on an email-verified key, no card,
  commercial use allowed. A new key works immediately for 1,000 calls
  before verification.
- **Map tiles are discounted**: one call per 50 vector-tile requests, and
  hosted style reads are public and unmetered.
- **Standard calls** (geocoding, car and other non-truck routing, matrix,
  isochrones) are 0.05p each from call 50,001 to 1 million, 0.03p to 10
  million and 0.02p beyond.
- **Monthly plans**: Starter £49 (250,000 calls), Growth £299 (2 million),
  Scale £1,250 (10 million).
- **Tiles you host yourself cost nothing on MapMap**: a PMTiles file on
  your bucket is read by the browser straight from your storage and never
  passes through the gateway.

## Next steps

- [Add your own data](/docs/custom-data): every source type, insertion points, feature-state and the native snippets
- [Examples](/docs/examples): copy-and-run samples for each pattern on this page
- [Web maps](/docs/maps): styles, Studio themes and the layer ids
- [Migrating from Mapbox: routing and APIs](/news/migrating-from-mapbox): endpoint mapping, the error model and the honest gap list
