Docs
Everything you need to go from a PostGIS table to a live map, and from a location to a geofence event.
Quickstart
- Create a read-only role for MapSocket (below) and allow our egress IPs if your database is firewalled.
- Create an account, then a project.
- Add a connection: paste the database URL. We check PostGIS, permissions and whether live updates will work.
- Create a source: choose tables, columns and a zoom range, then press Deploy source.
- 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:
-- 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.
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:
-- 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'); -- 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.
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:
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.
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.
| Endpoint | Key | What 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
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
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"withminutesand a travelmode: the area reachable from the center. Needs a routing engine on the server.- Address center
- Circles and isochrones take an
addressinstead ofcenterwhen the server has a geocoder. - Stop detection
stopDetection: trueholds the entry until a track saysstoppedorforeground, so drive-bys don't count.- Operating hours
operatingHoursper day in atimezone. Geofences reportcurrentlyOperating; with the project setting on, they send no events while closed.- Temporary geofences
disableAfterstops a geofence matching at a set time;deleteAfterremoves it.- Restrictions
userIdslimits a geofence to some users, andipto 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.
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:
{
"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.
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": { "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.