# GeoJSON and vector tiles

> Pick between paged entries.geojson and Mapbox vector tiles with TileJSON, filter tiles, handle caching and private projects, and use MapLibre GL JS.

Spatibase serves published map data in two forms: `entries.geojson`, a paged GeoJSON FeatureCollection with every field, and vector tiles (Mapbox Vector Tile, one layer named `entries`) described by a TileJSON document. This page is for developers putting Spatibase content into MapLibre GL, Mapbox GL, Leaflet, QGIS or ArcGIS.

Examples use `$API` for the project's Content Delivery API base URL; `$PROJECT_ID` is the UUID in the project's address in the web app:

```bash
export API=https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID
```

## Should I use GeoJSON or vector tiles?

Use GeoJSON for small and medium projects when you want every field in one request; use vector tiles for large projects and GIS tools, because they load only what is on screen. Both contain only the main locations of published entries of enabled types.

| | `entries.geojson` | Vector tiles |
|---|---|---|
| Content | every field, nested values intact | `id`, `type`, `title`, `color` and scalar fields only |
| Size | up to 5000 features per request (page for more) | any size, loaded tile by tile |
| Filters | every `/entries` parameter | `type`, `subtypes` and `group` only |
| Geometry | exact coordinates (7 decimals) | clipped and simplified per zoom level |
| Clients | Leaflet, MapLibre or Mapbox `geojson` source, QGIS, scripts | MapLibre or Mapbox `vector` source, QGIS |

To list every published entry as just id, type, title and bounding box, without fields, use `entries.index` ([compact index](https://spatibase.com/docs/reference/delivery-api/#entries-as-a-compact-index)); it pages like `entries.geojson`.

## GeoJSON

`entries.geojson` returns published entries that have a main location as a GeoJSON FeatureCollection:

```text
https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID/entries.geojson
```

- It takes every [`/entries` parameter](https://spatibase.com/docs/reference/delivery-api/#list-entries): `type`, `subtypes`, `group`, `q`, `filter[...]`, `bbox`, `near`, `radius`, `within`, `withinEntry`, `withinField`, `sort`, `page` and `include`. `limit` defaults to 1000, at most 5000.
- The response has `Content-Type: application/geo+json; charset=utf-8`, with the same caching and errors as the rest of the [Content Delivery API](https://spatibase.com/docs/reference/delivery-api/#caching).
- Left out: entries without a main location, entries of types with `locationMode: none` (even if a location is stored), drafts, unpublished entries and switched-off types. `meta.total` counts only the features that qualify.

To save a file:

```bash
curl -g -o entries.geojson "$API/entries.geojson?limit=5000"
```

### What does a GeoJSON feature contain?

Each feature has the entry id as its `id`, the main location as its `geometry`, and `properties` with `id`, `type`, `title` and then every field.

```bash
curl -sg "$API/entries.geojson?type=store-location&limit=1"
```

```json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": "192a713e-995a-4cb6-a84a-a5da45e9646a",
      "geometry": {"type": "Point", "coordinates": [-124.0828, 40.8665]},
      "properties": {
        "id": "192a713e-995a-4cb6-a84a-a5da45e9646a",
        "type": "store-location",
        "title": "Pacific Coast Outdoors – Arcata",
        "rental-phone": "+1 707-555-0135",
        "region": {"id": "66f36784-bb65-4de3-a745-bfd294cdf017", "type": "region", "title": "Northern California Coast"},
        "opens-at": "09:00",
        "delivery-area": {"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [...]}, "properties": {}}]}
      }
    }
  ],
  "meta": {"page": 1, "limit": 1, "total": 4, "totalPages": 4},
  "links": {
    "self": "https://api.spatibase.com/delivery/v1/projects/6806f635-636f-4814-ae20-988d4c84369e/entries.geojson?type=store-location&limit=1",
    "next": "https://api.spatibase.com/delivery/v1/projects/6806f635-636f-4814-ae20-988d4c84369e/entries.geojson?type=store-location&limit=1&page=2",
    "prev": null
  }
}
```

- Field values have the same shapes as in the JSON API ([Field values](https://spatibase.com/docs/reference/field-values/)). Element apiKeys cannot be `id`, `type` or `title`, so fields never overwrite them.
- Not included: `typePath`, `groups`, the timestamps and `distance`. `near` and `sort=distance` still filter and order the features. Use `/entries` when you need those keys.
- `geometry` is a `Point`, `LineString`, `Polygon` or a `Multi*` form, in WGS84 `[lng, lat]`.
- `meta` and `links` are GeoJSON "foreign members" (RFC 7946, section 6.1), which map libraries ignore. To get more than 5000 features, follow `links.next`, or use vector tiles.
- Nested values (objects and arrays) stay nested. MapLibre GL turns nested properties into JSON strings when it loads a `geojson` source, so `JSON.parse` them in event handlers, or load the entry from `/entries/<id>`.

## Vector tiles

Vector tiles serve published main locations as Mapbox Vector Tiles at this URL template:

```text
https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID/tiles/{z}/{x}/{y}.pbf
```

- XYZ scheme (y counted from the top, as in OpenStreetMap), Web Mercator, `z` from 0 to 22, `x` and `y` from 0 to 2^z - 1. Anything else is a `400` JSON error, such as `Tile z must be an integer between 0 and 22`; other extensions such as `.mvt` are a `404`.
- A tile with features is a `200` with `Content-Type: application/vnd.mapbox-vector-tile` (MVT version 2, not gzip-compressed). A tile with no features is `204 No Content`, with the same `Cache-Control` and `ETag` as tiles with data.
- Tiles are built on request from published main locations of enabled types whose `locationMode` is not `none`. For heavy traffic, put a CDN in front.

### Which layer and properties do tiles have?

Every tile has one layer named `entries` (extent 4096, buffer 64) with points, lines and polygons mixed. Geometries are clipped to the tile and simplified for the zoom level, so very small polygons and lines can disappear at low zoom. `Multi*` geometries become multi-part features.

**Large projects stay light at low zoom.** From zoom 0 to 8 a tile keeps one point per small grid cell (the entry with the lowest id stands for the others), so a world tile of a project with thousands of points stays well under 150 KB. Every point in those tiles has a `point_count` property, the number of entries it stands for (1 for a lone point). From zoom 9 on, tiles hold every point and have no `point_count`. Lines and polygons are not thinned, filters apply before thinning, and the same tile always gives the same bytes. Use `point_count` to size circles a little or to label a cluster; a click handler should still read the entry through `/entries/<id>`.

| Property | Type | Value |
|---|---|---|
| `id` | string | the entry id. The MVT feature id is not set; use `promoteId` for feature state |
| `type` | string | the type apiKey |
| `title` | string | the published title, at most 255 characters |
| `color` | string | the type's color (**Type color** in the app), `#rrggbb` |
| `point_count` | number | only at zoom 0 to 8, on points: how many entries this point stands for |
| `<apiKey>` | string, number or boolean | scalar fields of the kinds `text` (not the JSON editor), `number`, `boolean`, `date` (single values, not ranges), `choice`, `color`, `identifier` and `phone` |

- Strings are cut at 255 characters. Multiple choice values are joined with `", "`, for example `"amenities": "restrooms, parking, camping"`. Empty fields are absent, not `null`.
- Values follow the field's current settings, like the JSON output: a choice that is no longer an option is left out, and a field switched between single and multiple choice, or a date whose mode changed, shows the current shape. A field switched to a date range leaves the tiles.
- Not in tiles: `markdown`, `reference`, `link`, `images`, `files`, `contact`, `geo`, `embed`, `location`, JSON text and date ranges. Load the entry from `/entries/<id>` when someone clicks a feature.
- The fixed properties win over an element with the same apiKey; only `color` can clash.

### Filter tiles

Tiles accept `type` (including subtypes unless `subtypes=false`) and `group`, with the same rules and `400` errors as `/entries`. Every other parameter (`filter[...]`, `q`, `bbox`, `near`, `sort`, `include`) is ignored without an error.

```text
https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID/tiles/{z}/{x}/{y}.pbf?type=park
```

To filter by field values, use style expressions on the client, for example `['>=', ['get', 'rating'], 4.5]`.

### How are tiles cached?

Tiles use the Content Delivery API's caching headers (`Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400`) and an ETag that changes with the project's content version. A publish reaches map viewers within that cache window, and the tile URLs themselves never change.

## TileJSON

`tiles.json` is a TileJSON 3.0 document that tells a map library the tile URL, zoom range, bounds and field names. Point a vector source at it instead of hard-coding the tile template.

```bash
curl -s "$API/tiles.json"
```

```json
{
  "tilejson": "3.0.0",
  "name": "Pacific Coast Outdoors",
  "description": "Parks, regions, stores and trip ideas along the Pacific coast of California, Oregon and Washington.",
  "version": "12",
  "scheme": "xyz",
  "tiles": ["https://api.spatibase.com/delivery/v1/projects/6806f635-636f-4814-ae20-988d4c84369e/tiles/{z}/{x}/{y}.pbf"],
  "minzoom": 0,
  "maxzoom": 22,
  "bounds": [-124.85, 37.8, -122.45, 48.42],
  "center": [-123.65, 43.11, 6],
  "vector_layers": [{
    "id": "entries",
    "description": "Published entries with a main location",
    "fields": {"id": "String", "type": "String", "title": "String", "color": "String", "point_count": "Number", "slug": "String", "amenities": "String", "rating": "Number", "opens-at": "String"},
    "minzoom": 0,
    "maxzoom": 22
  }]
}
```

- `version` is the project's content version as a string. `description` is left out when the project has none.
- `bounds` is the extent of all published main locations; a project without any gets `[-180, -85.0511, 180, 85.0511]`. `center` is the middle of `bounds` at zoom 6.
- `fields` lists every tile property: `Number` for number fields, `Boolean` for boolean fields, `String` for the rest. It covers the scalar elements of all enabled types, so a field can be listed even if no tile contains it.
- The `tiles` template has no query string, except `?api_key=` when the TileJSON of a private project was requested with one. A source built from the TileJSON URL is therefore unfiltered; for a filtered source, pass the `tiles` array yourself (see the MapLibre example below).

## How do tiles and GeoJSON work in a private project?

Tiles, TileJSON and GeoJSON of a private project need an API key like every other Content Delivery API endpoint. Map libraries and QGIS load tile URLs without custom headers, so request `tiles.json?api_key=$API_KEY`: the `tiles` template in that response carries `?api_key=` as well.

- If you write the tile template yourself, append `?api_key=$API_KEY`, or `&api_key=$API_KEY` after `?type=...`.
- In MapLibre GL or Mapbox GL you can send a header instead, only to Spatibase URLs:

```js
const map = new maplibregl.Map({
  container: 'map',
  style: 'https://tiles.openfreemap.org/styles/liberty',
  transformRequest: url => url.startsWith('https://api.spatibase.com/delivery/')
    ? { url, headers: { 'X-API-Key': API_KEY } }
    : { url }
})
```

- Only read keys (for a website or app) work in `?api_key=`; edit keys for AI assistants are refused there.
- A key used in a web page is visible to anyone who can open the page. Give each site its own key. Details are in [Authentication](https://spatibase.com/docs/reference/authentication/#content-delivery-api-keys).

## MapLibre GL JS and Mapbox GL JS

MapLibre GL JS reads the TileJSON URL directly as a `vector` source. This snippet draws areas and points in their type colors, and adds a second source that only contains parks:

```js
const API = `https://api.spatibase.com/delivery/v1/projects/${PROJECT_ID}`

map.on('load', () => {
  map.addSource('spatibase', {
    type: 'vector',
    url: `${API}/tiles.json`,
    promoteId: { entries: 'id' }   // feature.id = entry id, for feature-state and click handling
  })
  map.addLayer({
    id: 'entry-areas', type: 'fill', source: 'spatibase', 'source-layer': 'entries',
    filter: ['==', ['geometry-type'], 'Polygon'],
    paint: { 'fill-color': ['get', 'color'], 'fill-opacity': 0.35 }
  })
  map.addLayer({
    id: 'entry-lines', type: 'line', source: 'spatibase', 'source-layer': 'entries',
    filter: ['==', ['geometry-type'], 'LineString'],
    paint: { 'line-color': ['get', 'color'], 'line-width': 3 }
  })
  map.addLayer({
    id: 'entry-points', type: 'circle', source: 'spatibase', 'source-layer': 'entries',
    filter: ['==', ['geometry-type'], 'Point'],
    paint: { 'circle-radius': 6, 'circle-color': ['get', 'color'] }
  })
  // Filtered tiles: pass the template directly, with the query string
  map.addSource('parks', {
    type: 'vector',
    tiles: [`${API}/tiles/{z}/{x}/{y}.pbf?type=park`],
    minzoom: 0, maxzoom: 22
  })
})
```

- The snippet assumes a map with a basemap style already loaded, for example `style: 'https://tiles.openfreemap.org/styles/liberty'` ([OpenFreeMap](https://openfreemap.org): free, no key). Spatibase serves your entries, not a basemap. Do not use `tile.openstreetmap.org` as a basemap in production; OpenStreetMap's public tile servers are not meant for apps.
- On click, `e.features[0].properties` holds the tile properties only. Fetch `/entries/<id>` for images, descriptions and other fields.
- Mapbox GL JS uses the same source and layer definitions (style specification v8); Mapbox GL JS itself needs a Mapbox access token.
- For a `geojson` source, set `data` to the `entries.geojson?limit=5000` URL. A complete page is in the [Quickstart](https://spatibase.com/docs/build/quickstart/#step-4-show-entries-on-a-maplibre-gl-map).
- Leaflet draws GeoJSON with `L.geoJSON`; the [Quickstart](https://spatibase.com/docs/build/quickstart/#step-5-show-entries-on-a-leaflet-map) has a complete page.

## QGIS

QGIS can load Spatibase as a vector tile layer (fast, few attributes) or as a GeoJSON layer (every attribute, a snapshot). The steps follow the QGIS 3 menus.

Vector tiles:

1. Choose **Layer**, **Add Layer**, **Add Vector Tile Layer…**.
2. Choose **New**, then **New Generic Connection…**.
3. Enter any name, and as URL the tile template `https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID/tiles/{z}/{x}/{y}.pbf` (add `?type=...` to filter, `?api_key=...` for a private project). Set the minimum zoom to `0` and the maximum zoom to `22`. Leave the style URL empty.
4. Click **OK**, then **Add**. QGIS draws the layer `entries` with a default style; style it by the `type` or `color` attributes.

GeoJSON with every attribute:

1. Choose **Layer**, **Add Layer**, **Add Vector Layer…**.
2. Set the source type to **Protocol: HTTP(S), cloud, etc.** with type **HTTP/HTTPS/FTP**.
3. As URI, enter `https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID/entries.geojson?limit=5000` (the default limit is 1000).
4. Click **Add**. Nested values such as images, references and geo fields are not flat attributes; depending on the QGIS version they appear as JSON text or are left out. The layer is a snapshot: reload it to see newly published content.

## ArcGIS

ArcGIS Online and ArcGIS Pro can read the GeoJSON URL, which is the simplest way in. In ArcGIS Online's Map Viewer, add a layer from a URL and give it `https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID/entries.geojson?limit=5000`. The Content Delivery API allows any origin, so the request works from the browser.

ArcGIS draws vector tiles through a style document (a Mapbox GL style JSON or a VectorTileServer), and Spatibase serves no style document. To use the tiles, write a style JSON with a `vector` source pointing at the tile template and layers on the source layer `entries`, host it yourself, and add that style's URL.

## Related

- [Preview your data](https://spatibase.com/docs/publish/preview-data/): a ready-made map page of the same published data, no code needed
- [Content Delivery API reference](https://spatibase.com/docs/reference/delivery-api/)
- [Quickstart](https://spatibase.com/docs/build/quickstart/)
- [Recipes](https://spatibase.com/docs/build/recipes/)
- [Authentication](https://spatibase.com/docs/reference/authentication/)
