Quickstart: a map in ten minutes

This quickstart takes you from an empty project to published entries on a MapLibre GL map, a Leaflet map and a vector-tile layer in about ten minutes. It is for developers; every code block is complete and ready to copy.

You need a Spatibase account, a browser, and curl and jq for the command-line steps.

Step 1: Get your project id

Every Content Delivery API URL contains the project’s id, a UUID. Open your project in the web app at https://app.spatibase.com and copy the id from the address bar: it is the part after /projects/, for example 61604e01-b862-437b-8151-9cb9353a4f57 in https://app.spatibase.com/projects/61604e01-b862-437b-8151-9cb9353a4f57/entries.

The project’s API page shows the same id inside ready-made URLs, with a Copy API address button. Keep it in a shell variable for the next steps:

bash
export PROJECT_ID=61604e01-b862-437b-8151-9cb9353a4f57   # your project's id
export API=https://api.spatibase.com/delivery/v1/projects/$PROJECT_ID

Note: This quickstart assumes the project’s Content Delivery API is public, which is the default. If the project is private, create a key under API, then Access & keys, and add -H "X-API-Key: $API_KEY" to every curl command (Authentication).

Step 2: Publish one entry

The Content Delivery API only returns published entries, so you need at least one. In the web app:

  1. Open Content model and click Create your first type. Name it “Cafe”, under Allowed shapes keep only Point ticked, and under the assigned elements click Assign element, then Create new element… to add a “Description” (Rich text) and a “Rating” (Number). Click Create type.
  2. Open Entries, click New above the list and choose “Cafe”. Enter the title “Bean There”, click Place a point and click the map. Fill in a description and a rating. Your changes save as a draft by themselves.
  3. Click Ready to publish, then click Publish in the side panel and confirm.

Any type and entry work; the rest of this page uses the apiKeys cafe, description and rating that Spatibase suggests for these names. A walkthrough in the app, with no code, is in Make your first map.

Step 3: Fetch entries and GeoJSON with curl

/entries returns published entries as JSON, 50 per page by default:

bash
curl -s "$API/entries" | jq -c '.data[0], .meta'
json
{"id":"d47e1a25-6c0f-4f7e-9a51-2b8c4e1f0a37","type":"cafe","typePath":["cafe"],"title":"Bean There","geometry":{"type":"Point","coordinates":[14.4208,50.0875]},"fields":{"description":"Small **coffee** bar.","rating":4.7},"groups":[],"createdAt":"2026-10-15T08:02:11.120Z","updatedAt":"2026-10-15T08:04:52.873Z","publishedAt":"2026-10-15T08:04:52.873Z"}
{"page":1,"limit":50,"total":1,"totalPages":1}

/entries.geojson returns the same entries as a GeoJSON FeatureCollection, ready for any map library. Feature properties are id, type, title and every field:

bash
curl -s "$API/entries.geojson" | jq -c '.features[0]'
json
{"type":"Feature","id":"d47e1a25-6c0f-4f7e-9a51-2b8c4e1f0a37","geometry":{"type":"Point","coordinates":[14.4208,50.0875]},"properties":{"id":"d47e1a25-6c0f-4f7e-9a51-2b8c4e1f0a37","type":"cafe","title":"Bean There","description":"Small **coffee** bar.","rating":4.7}}

Both responses allow any origin (CORS *) and carry an ETag, so a browser page can call them directly. After a publish, allow up to a minute for cached responses to refresh. Filters, paging and sorting are in the Content Delivery API reference.

Step 4: Show entries on a MapLibre GL map

Save this as maplibre.html, replace YOUR_PROJECT_ID, and open the file in a browser. A double-click is enough; no web server is needed.

html
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Spatibase + MapLibre GL</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" />
  <script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
  <style>html, body, #map { height: 100%; margin: 0 }</style>
</head>
<body>
<div id="map"></div>
<script>
  const PROJECT_ID = 'YOUR_PROJECT_ID'
  const API = `https://api.spatibase.com/delivery/v1/projects/${PROJECT_ID}`

  const map = new maplibregl.Map({
    container: 'map',
    style: 'https://tiles.openfreemap.org/styles/liberty',   // OpenFreeMap: free, no key, credits shown by MapLibre
    center: [0, 20],
    zoom: 1
  })

  map.on('load', async () => {
    const geojson = await (await fetch(`${API}/entries.geojson`)).json()
    map.addSource('entries', { type: 'geojson', data: geojson })
    map.addLayer({
      id: 'entries',
      type: 'circle',
      source: 'entries',
      filter: ['==', ['geometry-type'], 'Point'],
      paint: { 'circle-radius': 7, 'circle-color': '#e11d48', 'circle-stroke-width': 2, 'circle-stroke-color': '#fff' }
    })

    // Zoom to everything we got
    const bounds = new maplibregl.LngLatBounds()
    const extend = c => (typeof c[0] === 'number' ? bounds.extend(c) : c.forEach(extend))
    geojson.features.forEach(f => extend(f.geometry.coordinates))
    if (!bounds.isEmpty()) map.fitBounds(bounds, { padding: 60, maxZoom: 15, duration: 0 })

    map.on('click', 'entries', e => {
      new maplibregl.Popup().setLngLat(e.lngLat).setText(e.features[0].properties.title).addTo(map)
    })
  })
</script>
</body>
</html>

Lines and areas need a line or fill layer with a matching geometry-type filter; the vector tile example in step 6 and GeoJSON and vector tiles show both.

Step 5: Show entries on a Leaflet map

Save this as leaflet.html, replace YOUR_PROJECT_ID, and open it in a browser. It draws the same OpenFreeMap basemap with MapLibre inside Leaflet, which is why it loads three scripts.

html
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Spatibase + Leaflet</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
  <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" />
  <script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
  <script src="https://unpkg.com/@maplibre/maplibre-gl-leaflet@0.1.4/leaflet-maplibre-gl.js"></script>
  <style>html, body, #map { height: 100%; margin: 0 }</style>
</head>
<body>
<div id="map"></div>
<script>
  const PROJECT_ID = 'YOUR_PROJECT_ID'
  const API = `https://api.spatibase.com/delivery/v1/projects/${PROJECT_ID}`

  const map = L.map('map', { maxZoom: 19 }).setView([20, 0], 2)
  // OpenFreeMap vector basemap, drawn by MapLibre inside Leaflet (free, no key; the plugin shows its credits)
  L.maplibreGL({ style: 'https://tiles.openfreemap.org/styles/liberty' }).addTo(map)

  fetch(`${API}/entries.geojson`)
    .then(res => res.json())
    .then(geojson => {
      const layer = L.geoJSON(geojson, {
        pointToLayer: (feature, latlng) =>
          L.circleMarker(latlng, { radius: 7, color: '#fff', weight: 2, fillColor: '#e11d48', fillOpacity: 1 }),
        onEachFeature: (feature, layer) => {
          const el = document.createElement('strong')
          el.textContent = feature.properties.title   // textContent: titles are editor input
          layer.bindPopup(el)
        }
      }).addTo(map)
      if (layer.getBounds().isValid()) map.fitBounds(layer.getBounds(), { padding: [30, 30], maxZoom: 14 })
    })
</script>
</body>
</html>

Tip: OpenFreeMap is free and needs no key; keep its credits visible. Do not point production apps at tile.openstreetmap.org: OpenStreetMap’s public tile servers are not meant for that and block heavy use. Any other MapLibre style URL works the same way in the style option.

Step 6: Switch to vector tiles

Vector tiles load only what is on screen and carry each type’s color, so they suit large projects better than GeoJSON. entries.geojson returns up to 1000 entries per request by default and 5000 at most.

Keep the page from step 4 (same <head>, PROJECT_ID, API and map) and replace the whole map.on('load', ...) block with this:

js
  map.on('load', () => {
    // TileJSON tells MapLibre the tile URL, the bounds and the field names
    map.addSource('entries', { type: 'vector', url: `${API}/tiles.json` })
    map.addLayer({
      id: 'entries',
      type: 'circle',
      source: 'entries',
      'source-layer': 'entries',                    // the one layer in the tileset
      filter: ['==', ['geometry-type'], 'Point'],
      paint: {
        'circle-radius': 7,
        'circle-color': ['get', 'color'],           // every feature carries its type's color
        'circle-stroke-width': 2,
        'circle-stroke-color': '#fff'
      }
    })
    map.on('click', 'entries', e => {
      new maplibregl.Popup().setLngLat(e.lngLat).setText(e.features[0].properties.title).addTo(map)
    })
    // Zoom to the published content
    map.on('sourcedata', function once(e) {
      if (e.sourceId !== 'entries' || !map.getSource('entries').bounds) return
      map.off('sourcedata', once)
      const [w, s, e2, n] = map.getSource('entries').bounds
      map.fitBounds([[w, s], [e2, n]], { padding: 60, maxZoom: 15, duration: 0 })
    })
  })

tiles.json is a standard TileJSON 3.0 document. This is what MapLibre reads from it:

bash
curl -s "$API/tiles.json" | jq -c '{tiles, bounds, fields: .vector_layers[0].fields}'
json
{"tiles":["https://api.spatibase.com/delivery/v1/projects/61604e01-b862-437b-8151-9cb9353a4f57/tiles/{z}/{x}/{y}.pbf"],"bounds":[14.4208,50.0875,14.4208,50.0875],"fields":{"id":"String","type":"String","title":"String","color":"String","rating":"Number"}}

The tileset has one layer, entries. Each feature carries id, type, title, color and scalar fields such as rating; rich values such as images and descriptions stay out of tiles, so load /entries/<id> when someone clicks a feature.

Where to go next

Updated

Docs support

How can we help?

A person on our team reads every message. We include the page you are on, so you don't have to explain where.

Or write to hello@spatibase.com. For outages, check System status.

Talk to us

Not sure it’s a fit? Let’s find out

Tell us about your use case and what you need, from data model to security and deployment. In 30 minutes we’ll look at it together and tell you honestly if we can help.

Or write to hello@spatibase.com.