Docs

Everything you need to go from a PostGIS table to a live map, and from a location to a geofence event.

Quickstart

  1. Create a read-only role for MapSocket (below) and allow our egress IPs if your database is firewalled.
  2. Create an account, then a project.
  3. Add a connection: paste the database URL. We check PostGIS, permissions and whether live updates will work.
  4. Create a source: choose tables, columns and a zoom range, then press Deploy source.
  5. Copy the tile URL into your map. For live updates, run the generated trigger SQL.

Connect your database

MapSocket needs a role that can SELECT the tables you publish. A dedicated role keeps that explicit:

role.sql
-- A dedicated read-only role for MapSocket
CREATE ROLE mapsocket WITH LOGIN PASSWORD 'use-a-long-random-password';
GRANT USAGE ON SCHEMA public TO mapsocket;
GRANT SELECT ON public.parcels, public.roads TO mapsocket;

Then paste a connection URL. We default to sslmode=require.

Connection URL
postgres://mapsocket:…@db.example.com:5432/geodata?sslmode=require

Use the direct connection, not the pooler. Live updates rely on LISTEN, which transaction-mode poolers (PgBouncer, Supabase's pooler on port 6543, Neon's -pooler hosts) don't support. The connection check flags pooled URLs. If a pooler is your only option, choose snapshot mode.

Connection strings are encrypted with AES-256-GCM at rest and are never returned by the API. Only host, port, database and user are shown afterwards.

Live updates

In live mode, a trigger on each table sends a pg_notify on the tile_update channel when a row changes. MapSocket batches those notifications, works out which tiles held the old and new geometry, and rewrites only those tiles at every zoom level.

The dashboard generates a mapsocket_notify_tile_update() function using your real id and geometry columns. Run it once, then attach a trigger to each table:

trigger.sql
-- The dashboard generates mapsocket_notify_tile_update() once per database,
-- plus one trigger per published table (layer name, id column, geometry column).
-- Run it once as the table owner:
CREATE TRIGGER mapsocket_parcels
  AFTER INSERT OR UPDATE OR DELETE ON public.parcels
  FOR EACH ROW EXECUTE FUNCTION
    mapsocket_notify_tile_update('parcels', 'id', 'geom');
Notification payload
-- What the trigger sends on the tile_update channel
{"layer":"parcels","id":4182,"op":"update","old_bounds":{"min_lon":-73.9912,"min_lat":40.7402,"max_lon":-73.9881,"max_lat":40.7419}}

The layer name in the trigger must match the layer name in the source. Creating a trigger needs table-owner rights once; the MapSocket role itself stays read-only.

Snapshot mode

No triggers, no LISTEN. The source rebuilds on a schedule you set, from every 5 minutes to once a week. Good for read replicas, pooled URLs and data that changes nightly. The Sandbox plan rebuilds every 24 hours.

Tile URLs and keys

Every source gets three URLs. All take a publishable key as ?key=.

/t/acme/parcels.json
TileJSON with the zoom range, layers and the tile template
/t/acme/parcels/{z}/{x}/{y}.pbf
Mapbox Vector Tiles, gzipped, with ETags
/t/acme/parcels/events
Server-sent events when tiles change

Label points for polygons

Polygon layers can carry a companion point layer, parcels_labels, with one point per feature and the same id and properties, so labels and icons don't need client-side math. Choose how the point is placed: point on surface (ST_PointOnSurface, always inside the shape, the default), centroid (ST_Centroid, the center of mass, which can land outside C-shapes and multi-part features), or a point column you already keep in the table. Label points update with the polygons when rows change.

All three live on https://tiles.mapsocket.com, or your own domain on Pro. Publishable keys (pk_live_…) are safe in browser code; lock each one to the origins that may use it. Secret keys (sk_live_…) belong on servers only.

MapLibre GL
map.addSource("parcels", {
  type: "vector",
  url: "https://tiles.mapsocket.com/t/acme/parcels.json?key=pk_live_…",
});

map.addLayer({
  id: "parcels",
  type: "fill",
  source: "parcels",
  "source-layer": "parcels",
  paint: { "fill-color": "#2D6E9E", "fill-opacity": 0.3 },
});

Refresh the map live

Tiles are cached, so a map won't see an edit until it asks again. Subscribe to the events stream and swap the tile URL when tilefeed reports new tiles:

live-refresh.js
const tiles = "https://tiles.mapsocket.com/t/acme/parcels/{z}/{x}/{y}.pbf?key=pk_live_…";
const events = new EventSource("https://tiles.mapsocket.com/t/acme/parcels/events?key=pk_live_…");

events.addEventListener("update_complete", (e) => {
  // { source, tiles_updated, affected_zooms, max_zoom, layers_affected }
  const update = JSON.parse(e.data);
  map.getSource("parcels").setTiles([`${tiles}&_t=${Date.now()}`]);
});

// Sent after a full rebuild (deploys and snapshot refreshes)
events.addEventListener("generate_complete", () => {
  map.getSource("parcels").setTiles([`${tiles}&_t=${Date.now()}`]);
});

affected_zooms and max_zoom let you skip refreshes that don't touch the zoom level the user is looking at.

Contour lines

Every project can add elevation contours, worldwide, under or over its own layers. Point a vector source at https://tiles.mapsocket.com/t/terrain/contours.json?key=pk_live_… and style the contours layer: each line has ele (meters, or feet with units=ft) and level, which is 1 on every fifth, index line. Spacing follows the zoom, from 200 m at zoom 8 down to 5 m at zoom 15, or set your own with interval. GET /v1/elevation answers the height at up to 100 points.

contours.js
map.addSource("contours", { type: "vector", url: "https://tiles.mapsocket.com/t/terrain/contours.json?key=pk_live_…" });
map.addLayer({
  id: "contours",
  type: "line",
  source: "contours",
  "source-layer": "contours",
  paint: {
    "line-color": "#8C6A3A",
    "line-width": ["match", ["get", "level"], 1, 1.2, 0.5],
  },
});

The reference has the full spacing table. The dashboard's Contours page previews the layer and gives you the URL with your key.

Georeferenced maps

Put an old survey, a site plan or a drone photo on the map under your live data. In the dashboard's Georeferencer, upload the image and click matching points on it and on the map, at least three, spread across the image. Each point shows how far off it is in meters, so a mistyped point stands out. Choose affine for scans that are only shifted, rotated and scaled, polynomial for older or warped paper, or thin-plate spline to bend the image through every point. Crop away the margin, publish, and add the raster tile URL to your map like any other source.

The reference has the URLs; the warped GeoTIFF is a download in the dashboard.

Geofencing API

Base URL https://api.mapsocket.com. Send the key in the Authorization header. Coordinates are WGS84; GeoJSON uses [lng, lat] order. Parameters and response shapes for every endpoint are in the API reference. Project-wide behaviour (stop detection, buffers, places, regions, trip and fraud thresholds) is set under Geofencing settings in the dashboard.

EndpointKeyWhat it does
POST /v1/track pk or sk Record a location. Returns the user, new events, geofences, place, country and state.
GET /v1/context?coordinates=lat,lng pk or sk Geofences, place and region at a point. Stateless; no events.
GET /v1/search/geofences · /v1/search/places pk or sk Nearest geofences or places, with distance in meters.
POST /v1/geofences/check pk or sk Which geofences contain each of up to 1,000 points.
POST · PATCH /v1/trips · /v1/trips/:id pk or sk Start a trip, or move it to a new stage.
GET · POST · PUT · DELETE /v1/geofences… sk List, create, get, upsert, delete and import geofences; users inside one.
GET · DELETE /v1/trips… sk List, get and delete trips.
GET · POST · PUT · DELETE /v1/beacons… · /v1/places… sk Manage beacons and your own places.
GET · POST · DELETE /v1/events… sk List, get, verify and delete events.
GET · DELETE /v1/users… · /v1/search/users sk List, find, read and delete tracked users.
POST /v1/fraud/block · /v1/fraud/unblock sk Block or unblock users.

Track a location

Request
curl https://api.mapsocket.com/v1/track \
  -H "Authorization: pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "driver-118",
    "deviceId": "ios-7F3A",
    "latitude": 40.7411,
    "longitude": -73.9897,
    "accuracy": 12
  }'

MapSocket compares the geofences containing this point with the user's previous set and returns an event for each change:

user.entered_geofence
The point is inside a geofence the user wasn't in before
user.exited_geofence
The user was inside and now isn't
user.dwelled_in_geofence
Sent once per visit after the geofence's dwell threshold

The same call also reports place visits, country and state changes, beacons in range and trip progress, each as its own event type (full list). Entries carry a confidence of 3 (fully inside, allowing for accuracy), 2 (inside, but the accuracy circle crosses the edge) or 1 (just outside, with buffered entries on). Fixes less accurate than the project's maximum, 1,000 meters by default, update the location but don't fire events, so one bad GPS reading can't produce a false exit.

Create a geofence

Upsert by tag and external id
curl -X PUT https://api.mapsocket.com/v1/geofences/loading-bay/flatiron-02 \
  -H "Authorization: sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "circle",
    "center": { "type": "Point", "coordinates": [-73.9897, 40.7411] },
    "radius": 80,
    "description": "Flatiron loading bay",
    "dwellThresholdMinutes": 10,
    "metadata": { "dock": 2 }
  }'

Polygons use "type": "polygon" and a GeoJSON Polygon or MultiPolygon in geometry. To keep geofences in step with a PostGIS table, set up a sync in the dashboard: choose the table, the id column, and a fixed tag or a tag column. Points and lines can be buffered into zones. Already on Radar? POST /v1/geofences/import takes Radar's CSV format, including metadata.{key} and operatingHours.{day} columns.

Geofence options

Every geofence, whichever way it was created, can also have:

Isochrone shape
"type": "isochrone" with minutes and a travel mode: the area reachable from the center. Needs a routing engine on the server.
Address center
Circles and isochrones take an address instead of center when the server has a geocoder.
Stop detection
stopDetection: true holds the entry until a track says stopped or foreground, so drive-bys don't count.
Operating hours
operatingHours per day in a timezone. Geofences report currentlyOperating; with the project setting on, they send no events while closed.
Temporary geofences
disableAfter stops a geofence matching at a set time; deleteAfter removes it.
Restrictions
userIds limits a geofence to some users, and ip to tracks from some IPs or ranges (for example, an office network).

Field details are on the create geofence reference.

Trips

Start a trip from the app when a user sets off, naming the destination geofence. Every track then refreshes the ETA, and MapSocket moves the trip from started to approaching (ETA under the threshold, 5 minutes by default) to arrived (inside the destination), with an event at each step. With a scheduledArrivalAt, trips also report how late they'll be. Complete or cancel the trip when you're done; idle trips expire after 24 hours.

Start a trip
curl -X POST https://api.mapsocket.com/v1/trips \
  -H "Authorization: pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "order-1041",
    "userId": "driver-118",
    "mode": "car",
    "destinationGeofenceTag": "store",
    "destinationGeofenceExternalId": "flatiron"
  }'

ETAs use a routing engine when the server has one, and a straight-line estimate otherwise. See Trips in the reference.

Places

Turn on places in the geofencing settings to learn when users visit shops, restaurants, fuel stations and other points of interest, without drawing geofences. Places come from OpenStreetMap, with chains from its brand tag, plus any you add with POST /v1/places. A visit needs the user to be stopped, and you can limit visits to some chains or categories. Places reference.

Regions

Each track resolves the user's country and, for the US, Canada, Australia, Brazil, China, India, Indonesia, Russia and South Africa, their state or province, and sends entry and exit events when they change. Useful for tax, compliance and regional content. Regions reference.

Beacons

Register iBeacon or Eddystone beacons, then include the beacons your app ranges in each track. You get entry and exit events per beacon, for door-level precision indoors. Beacons reference.

Fraud checks

Every track is checked for impossible jumps, mock location providers (reported by your app), poor accuracy and countries outside your allow or block list. The result is on the user as fraud, with reason codes. Check user.fraud.passed before granting anything on a location, block users with POST /v1/fraud/block, and optionally stop failing fixes from firing events. Fraud reference.

Webhooks

Add an endpoint in the dashboard and choose which event types it receives (all except user.updated_trip by default). Each delivery is a POST with the event and the user as they are after it:

Webhook body
{
  "id": "evt_6b1d…",
  "type": "user.dwelled_in_geofence",
  "createdAt": "2026-11-04T14:12:40Z",
  "projectId": "c2a7…",
  "event": {
    "type": "user.dwelled_in_geofence",
    "userId": "driver-118",
    "geofence": { "tag": "loading-bay", "externalId": "flatiron-02" },
    "location": { "type": "Point", "coordinates": [-73.9896, 40.7412] },
    "confidence": 3,
    "duration": 10
  },
  "user": { "userId": "driver-118", "geofences": […], "trip": null, "fraud": { "passed": true, … } }
}

Every request carries MapSocket-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of t, a period, and the raw body, keyed with the endpoint's signing secret. Reject old timestamps to stop replays.

Verify in Node (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const secret = process.env.MAPSOCKET_WEBHOOK_SECRET;

// Use the raw body: the signature covers the exact bytes we sent.
app.post("/hooks/mapsocket", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("MapSocket-Signature") ?? "";
  const { t, v1 } = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${req.body.toString("utf8")}`)
    .digest("hex");

  const valid = v1?.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  if (!valid || !fresh) return res.status(400).send("bad signature");

  const { type, event } = JSON.parse(req.body);
  console.log(type, event.userId, event.geofence?.externalId);
  res.sendStatus(204);
});

Failed deliveries are retried with backoff, and every attempt is listed under the endpoint in the dashboard.

Errors

Errors use a matching HTTP status and the same shape everywhere. The message says what to change.

Error body
{ "error": { "code": "plan_limit", "message": "You've reached your plan's geofence limit. Upgrade to add more." } }

Run it yourself

MapSocket's tile engine is tilefeed, MIT licensed. Each source has an Export config action that produces the equivalent tilefeed TOML (password removed), and you can download the MBTiles file at any time. Install with brew install muimsd/tilefeed/tilefeed or cargo install tilefeed, then run tilefeed serve.