Skip to content
06Platform

Data & live sources

Formats and ingest, field inference, time fields, the live source catalog, URL/REST/webhook/MCP connections, refresh policies, query policy and limits.

The library

Data in the workspace rail holds every dataset and every live connection. Datasets are shared by all projects in the workspace; a project references the ones it uses.

The Data library: datasets with geometry type, feature and field counts, serving mode, usage and live status.
  1. Upload: a file from your disk, up to 50 MB.
  2. From URL: a GeoJSON, PMTiles, XYZ or TileJSON address, optionally refreshing.
  3. REST API: an authenticated endpoint: method, headers, query parameters, a records path and a field map.
  4. MCP source: a tool on your own MCP server becomes a dataset.
  5. Connections: the live sources behind these datasets, with health and refresh history.
  6. Filter: matches dataset names as you type.
  7. Geometry: points, lines, polygons or mixed, detected at ingest.
  8. Fields: how many attributes survived ingest, each with a type, a role and statistics.
  9. Serve: geojson below roughly 5,000 features, tiles above it.
  10. Status: LIVE when a connection refreshes it, FROZEN for an uploaded file.

Formats

SourceHowNotes
GeoJSONupload or URLthe reference format; URL sources can refresh
Shapefile.zip upload.shp + .dbf + .prj in one archive; reprojected to WGS84
CSV / XYuploadlongitude and latitude columns, or an address column (geocoded)
KML · KMZuploadplacemarks, folders, styles are dropped in favour of Spatly styling
GPXuploadtracks and waypoints, both ingested
PMTilesupload or URLserved straight from object storage
Vector tilesXYZ or TileJSON URLa sourceLayer name is required
Raster XYZ · WMS · WMTSURLdrawn as a raster layer, not ingested
Feature services (REST)URLread-only, paginated
REST APIURL + authheaders, records path, field map
WebhookpushPOST /api/webhooks/data/:connectionId
MCP sourceyour own servera tool result becomes a dataset

3D coordinates are flattened to 2D on ingest, so a Z value in a GeoJSON point or a GPX track will not stop the load.

Ingest and field inference

GeoJSON, CSV, KML, GPX and Shapefile are parsed in process; everything else goes through GDAL. Geometries land in PostGIS as WGS84 ([lng, lat]).

Every field is then sampled for:

  • type: string, number, integer, boolean, datetime
  • role: id, label, category, measure, time, lng, lat
  • statistics: min, max, distinct count, null count

That inference is what the rest of the product reads: the classification methods propose breaks from min/max and distinct counts, field pickers only offer numeric fields where a number is required, and label fields become the default popup title.

You can correct any of it in the dataset drawer. A code stored as a number is the usual case.

Time fields

A dataset with a datetime field can carry a time field. Setting it turns on:

  • time-enabled layers (layer.timeField) that clip to the beat's time window
  • dateRangeWidget presets and the mini timeline brush
  • timeControl playback
  • query_timeseries and the timeseries option on metrics

Set it at upload time (options.timeField) or later in the dataset drawer.

Serving: GeoJSON or tiles

SizeServed asEndpoint
≲ 5,000 featuresone GeoJSON document, ETag-cachedGET /api/v1/datasets/:id/geojson?bbox=&limit=&fields=
above thatMapbox Vector Tiles from ST_AsMVTGET /api/v1/tiles/:datasetId/{z}/{x}/{y}

The choice is automatic and shows in the Serve column. It matters in one place: on a tiled dataset the client only holds what is in the viewport, so client-side feature counts describe the viewport. Metrics are computed server-side and stay correct either way.

Address geocoding

A CSV with an address column and no coordinates is geocoded at ingest: up to 500 rows, one request per second, against a cached Nominatim proxy. Rows that fail are flagged, not dropped: fix the address in the dataset drawer and re-geocode.

Geocoding is also available directly:

bash
curl -H "Authorization: Bearer $TOKEN" "$BASE/geocode?q=Antakya&limit=1"
curl -H "Authorization: Bearer $TOKEN" "$BASE/reverse?lng=36.16&lat=36.20"

Results are cached for seven days. Attribution (© OpenStreetMap contributors) is required wherever they are shown.

Live connections

The Connections tab: live sources with their kind, refresh policy, health and last success.
  1. Connections tab: everything that refreshes, separate from the datasets it feeds.
  2. Kind and target: url, rest, xyz, pmtiles, feature service, wms, webhook or mcp.
  3. Refresh: interval or cron, and when it last succeeded.
  4. Health: consecutive failures and the last error, verbatim.
KindWhat it is
urla document Spatly re-fetches: GeoJSON, CSV, KML, GPX
restan authenticated JSON API: method, headers, query params, body template, recordsPath, lngField/latField/idField
xyz · wms · pmtilestile sources; drawn, not ingested
arcgis_featurea read-only feature service, paginated
webhookyou push; Spatly stores
mcpa tool on your MCP server, called on the refresh interval

Secrets (bearer tokens, header values) are encrypted at rest with CONNECTION_SECRET_KEY and never reach a viewer.

Refresh policies

json
{ "mode": "interval", "intervalSec": 60 }
{ "mode": "schedule", "cron": "*/15 * * * *" }
{ "mode": "webhook" }
{ "mode": "static" }

The minimum interval is 15 s. Sources are fetched server-side and cached, so a thousand viewers cost the upstream one request per interval, not a thousand.

The scheduler runs a tick every 30 s (SPATLY_SCHEDULER=off disables it), takes a lock so only one instance refreshes, runs at most five refreshes in parallel, and evaluates alerts after each one. For a serverless deployment, drive it from an external cron instead:

bash
curl "https://spatly.io/api/internal/cron?key=$CRON_KEY"

Health and stale state

Every connection carries lastSuccessAt, lastErrorAt, lastError, latencyMs and consecutiveFailures. After three consecutive failures the dependent widgets go stale: hatched, with last success N minutes ago. Nothing is silently wrong.

Webhooks

Create a connection of kind webhook and push into its URL. mode=replace (the default) makes the payload the current state; mode=append adds to it.

bash
curl -X POST "https://spatly.io/api/webhooks/data/$CONNECTION_ID?secret=$WEBHOOK_SECRET&mode=replace" \
  -H "Content-Type: application/json" \
  --data @vehicles.geojson
# → { "received": 128, "featureCount": 128, "mode": "replace" }

The secret can travel as ?secret=, as X-Spatly-Secret, or as a bearer token. Both GeoJSON and a bare array of records are accepted; records need lngField/latField set on the connection.

Live source catalog

One-click public feeds, each with a refresh interval, a suggested style and ready-made metrics:

PresetRefreshMetrics it creates
USGS earthquakes · past day5 mincount · strongest (M) · M4.5+ · by magnitude type
USGS earthquakes · past 7 days15 mincount · strongest · median depth · M5+
NASA EONET · open natural events30 minopen events · largest magnitude
NWS active weather alerts (US)5 minactive alerts · by event · by severity · extreme + severe
OpenSky · aircraft positions15 minaircraft tracked · airborne · average ground speed · by country
GDACS · disaster alerts (Orange/Red)30 minactive alerts · red alerts · by hazard · by alert level

Each becomes an ordinary connection you can edit afterwards. OpenSky is rate-limited upstream, so 15 minutes is the floor.

MCP sources

Data → MCP source: paste the server URL (streamable HTTP), add a bearer token if it needs one, press Discover tools (tools/list), pick a tool and its JSON arguments, map the result (recordsPath, lngField/latField, idField), test, save.

Spatly then calls that tool on the refresh interval and turns the result (structuredContent, or the first JSON text block) into a dataset like any other. Layers, metrics and alerts work unchanged. See MCP server for the other direction, where agents read your workspace.

Query policy and the agent budget

Each connection has a query policy:

KeyDefaultEffect
maxLimit1000the most rows one query may return
requireBboxfalserefuse a feature query with no bounding box
rateLimitPerMin120per connection
allowedFieldsalla whitelist; other fields are not returned
agentAccessibletruewhen false, Spatly's own MCP server does not expose it

On top of that, the workspace agent budget (Settings → General) bounds every token: maxCallsPerMin (60), maxRowsPerCall (500), requireBbox. The tighter of the two wins. Lists page with nextCursor; nothing is ever dumped wholesale.

Limits

LimitValue
Upload size50 MB per file
Address geocoding at ingest500 rows, 1 request/second
Minimum refresh interval15 s
Datasets and storageper plan, see Plans & limits
Live bindingsper plan (2 on Free, 25 on Pro, 200 on Team)
Public-id read rate600 requests/minute per published surface

Verified on 2026-09-02 against the local development build (15 seeded datasets across CSV, GeoJSON, KML, GPX, zipped Shapefile and a live URL feed).

Was this page helpful?

Something wrong or missing? Write to hello@spatly.io.