Releases: a-saed/datum
v0.13.0 — MCP server
MCP server for AI agents
datum-sync now ships a Model Context Protocol stdio server. Point Claude Desktop, Cursor, Windsurf, or any MCP-compatible agent at your synced PostGIS data.
New: datum-mcp CLI
npx datum-sync datum-mcp ws://localhost:3000/ws --table featuresOr install globally and add to Claude Desktop config:
{
"mcpServers": {
"datum": {
"command": "datum-mcp",
"args": ["ws://localhost:3000/ws", "--table", "features"]
}
}
}Three tools exposed
| Tool | Description |
|---|---|
query |
Full PostGIS SQL against local PGlite — ST_Distance, ST_Within, ST_AsGeoJSON, everything |
get_schema |
Table columns, PostgreSQL types, datum roles |
get_status |
Connection state, pending writes, row count |
Read-only by default
Mutations are blocked unless you pass --allow-writes. Safe to give to an AI agent.
Zero impact on existing users
Shipped as a separate datum-sync/mcp entrypoint. Nothing changes if you don't use it.
See the MCP Server docs for setup and full reference.
v0.12.0 — Live bbox tracking (mapBbox)
What's new
mapBbox() — eliminate the viewport wiring boilerplate
Every spatial app used to write this manually:
const b = map.getBounds()
const db = await DatumClient.connect({
serverUrl,
bbox: [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()],
})
map.on('moveend', () => {
const b = map.getBounds()
db.setBbox([b.getWest(), b.getSouth(), b.getEast(), b.getNorth()])
})Now:
import { DatumClient, mapBbox } from 'datum-sync'
const db = await DatumClient.connect({
serverUrl,
bbox: mapBbox(map), // auto-tracks map moves — done
})Works out-of-the-box with MapLibre GL, Mapbox GL, and Leaflet. Custom options for Google Maps, OpenLayers, and anything else:
// Google Maps
bbox: mapBbox(map, { event: 'idle', getBbox: m => { const b = m.getBounds().toJSON(); return [b.west, b.south, b.east, b.north] } })React hook in datum-sync/react:
import { useMapBbox } from 'datum-sync/react'
bbox: useMapBbox(mapRef.current)BboxSource interface
For GPS inputs, custom viewports, or any live source:
bbox: {
initial: () => [west, south, east, north],
subscribe: (onChange) => {
const id = setInterval(() => onChange(getViewport()), 1000)
return () => clearInterval(id)
}
}Static bbox arrays still work unchanged — 100% backwards compatible.
Install
npm install datum-sync@0.12.0v0.11.1 — Gone notifications
What's new
Gone notifications (patch)
When a feature is updated and the new state no longer matches a client's where subscription predicate, the server now sends a synthetic delete delta to that client — so the stale row is removed from the local PGlite database immediately.
Before: stale data stayed in the client's local DB until the next full reconnect.
After: the client receives { type: "delta", op: "delete", feature: { id: "..." } } and removes the row automatically.
Scope:
updateoperations that leave the predicate → gone notification sentinsertoperations that never matched → still silently dropped (row was never in the client's DB)deleteoperations → unchanged (existing delete delta already handles this)
Server-only patch. No client changes required.
Docker
docker pull ghcr.io/a-saed/datum-server:0.11.1
docker pull ghcr.io/a-saed/datum-server:latestv0.11.0 — Non-spatial tables
What's new
Non-spatial table support
datum now syncs any Postgres table — not just tables with a PostGIS geometry column.
Spatial table (existing behavior — unchanged):
const db = await DatumClient.connect({
serverUrl,
bbox: [-122.5, 37.7, -122.4, 37.8], // required for spatial tables
table: 'features',
})Non-spatial table (new):
const db = await DatumClient.connect({
serverUrl,
table: 'messages', // no bbox needed
where: "project_id = $1",
whereParams: [projectId],
})Use case: spatial apps typically have both spatial layers (features, parcels) and non-spatial supporting data (users, projects, settings, messages). datum now handles both without a second sync library.
Everything works identically for non-spatial tables:
- Real-time deltas
- Typed column support + schema cloning
- Subscription predicates (
where/whereParams) - JWT auth + per-delta RLS checks
- DevTools (SQL REPL, Schema inspector, Status tab)
- Outbox pattern, offline resilience, fast reconnect
Backwards compatible — all existing spatial configurations are unchanged. bbox simply becomes optional in DatumConfig.
Install
npm install datum-sync@0.11.0
docker pull ghcr.io/a-saed/datum-server:latestv0.10.0 — Per-delta RLS check
What's new
Per-delta RLS check
When JWT auth is configured, datum now verifies Postgres Row Level Security policies before broadcasting each delta to an authenticated client. If a user loses access to a row mid-session — role change, policy update, resource revocation — future deltas for that row are silently dropped for that client.
No configuration required. Runs automatically whenever auth: is set in datum.yaml.
Fail-open: if the RLS check query fails due to a transient DB error, the delta is sent rather than silently dropped. This ensures transient failures never cause unexplained data gaps on the client.
Delete deltas are always forwarded — a deleted row cannot be queried through RLS, so datum conservatively sends the delete to let clients clean up their local state.
Upgrade notes
Server-only change. No client updates required. datum-sync version unchanged.
Docker
docker pull ghcr.io/a-saed/datum-server:latestv0.9.0 — Subscription predicates
What's new
Subscription predicates
Filter your sync subscription server-side with any SQL expression — including PostGIS operators.
// Developer-written predicate
const db = await DatumClient.connect({
serverUrl,
bbox: [-122.5, 37.7, -122.4, 37.8],
where: "type = 'building' AND height > 10",
})
// User-derived values — injection-safe bound parameters
const db = await DatumClient.connect({
serverUrl,
bbox,
where: "type = $1 AND score > $2",
whereParams: [userSelectedType, minScore],
})
// PostGIS operators work naturally
const db = await DatumClient.connect({
serverUrl,
bbox,
where: "ST_DWithin(geom, ST_MakePoint(\$1, \$2)::geography, \$3)",
whereParams: [lng, lat, radiusMeters],
})Three-layer security:
- Keyword blocklist — rejects
SELECT,DROP,dblink,pg_read_file, etc. before touching the database EXPLAINin aREAD ONLYtransaction — validates syntax and column references via Postgres's own parser- pgx bound parameters —
whereParamsvalues are never interpolated into SQL strings
The predicate is applied to both the initial snapshot and real-time delta routing. Clients without a predicate are unaffected — fully backwards compatible.
Install
npm install datum-sync@0.9.0docker pull ghcr.io/a-saed/datum-server:latestv0.8.0 — DevTools
What's new
datum DevTools (datum-sync/devtools)
A floating browser panel for inspecting the local PGlite database, schema, and sync state while building. Zero production bundle impact.
const db = await DatumClient.connect({ ... })
if (import.meta.env.DEV) {
const { initDatumDevtools } = await import('datum-sync/devtools')
initDatumDevtools(db)
}Three tabs:
- Query — full SQL REPL against local PGlite + PostGIS.
Cmd+Enterto run. - Schema — column inspector with types, role badges, and schema hash.
- Status — connection state, pending writes, schema diff notice when columns change.
Ctrl+Shift+D to toggle. Draggable resize. Persists open/closed state.
Multi-table support: initDatumDevtools([featuresDb, waypointsDb]) — client switcher appears automatically.
Try it: open the live demo and press Ctrl+Shift+D.
New client APIs
DatumClient.onSchemaChange(cb)— post-connect subscription, fires with{ prev, next }columns when local DB wipes. Also available inDatumConfig.client.tableName— public getter for the configured table name.client.columns— public getter for the current column list.
Install
npm install datum-sync@0.8.0docker pull ghcr.io/a-saed/datum-server:latestv0.7.1 — Schema cloning + typed column support
What's new
Schema cloning
datum now auto-introspects your PostGIS table at startup and mirrors its exact column structure in the local PGlite database — no extra configuration needed.
- Typed columns — any columns beyond the 4 required ones (
id,geom,updated_at,properties) are automatically synced. Queryname,height,score, etc. with normal SQL on both sides. propertiesJSONB is optional — it coexists with typed columns. Use both together or drop the bag entirely.- Hash-based invalidation — when the server schema changes (e.g.
ALTER TABLE ... ADD COLUMN), clients detect the mismatch on next connect, wipe local IndexedDB, and re-sync automatically. - Dynamic trigger — the server's NOTIFY trigger is rebuilt at startup to include all columns in the payload, keeping real-time delta sync working for any schema.
Demo update
The live demo now uses typed name and type columns alongside a properties JSONB bag, with a hover popup that reads both.
Upgrade notes
Existing clients upgrading from 0.6.x — the local schema_version bumps from '2' to '3'. On first connect to a 0.7.x server each client will wipe its IndexedDB and perform a full re-sync. No server-side data is lost.
Install
npm install datum-sync@0.7.1docker pull ghcr.io/a-saed/datum-server:latestv0.6.0 — Per-user JWT authentication
What's new
Opt-in per-user authentication using JWT tokens.
Server
- Configure
auth.jwt_algorithm(HS256/RS256/ES256) and setJWT_SECRETenv var orjwt_public_keyfile path - All JWT claims forwarded to Postgres as
datum.<claim>session variables before every snapshot query and write — your RLS policies do the actual row filtering - Startup warning when connected as superuser (RLS would be bypassed)
- 4401 close code on missing or invalid token
- Mid-connection token refresh via
authwire message
Client
token?: string | () => Promise<string>inDatumClient.connect()config- Function tokens auto-refresh 60 seconds before JWT expiry — no reconnect, no snapshot re-fetch
Docs
- New
docs/auth.md— Postgres role setup, RLS policy examples, security notes - API reference updated with
tokenconfig option and auth wire messages
Backwards compatible
No auth: block in config and no JWT_SECRET env var = datum works exactly as before.
Deferred (designed, not yet implemented)
- Webhook auth mode (
auth.mode: webhook) — verify opaque tokens against your app's endpoint - Per-delta RLS check (
auth.broadcast_rls_check: true) — RLS-filter real-time delta messages before broadcast
v0.5.0 — Security & correctness hardening
What's new
This release hardens the client and server based on a full architectural review. No breaking changes to the public API.
Client
- Ack-based sync — writes are held in-flight until the server sends an
ack. If the connection drops mid-flight the writes are automatically retried on reconnect. Previously writes were marked synced optimistically. - Immediate flush on connect — pending outbox writes are sent as soon as the connection is established, not after the first sync interval (up to 5 s later).
- Correct insertion ordering — outbox now uses a
BIGSERIAL seqcolumn so writes are always drained in the order they were made, not by feature timestamp. - Table name validation — invalid table names are rejected at construction time with a clear error.
- Millisecond-precision
since— catch-up subscribe now sendsupdated_atwith millisecond precision, preventing missed writes when two changes share the same second. - Atomic snapshot load — snapshot writes are wrapped in
BEGIN/COMMIT. exec()removed from public API — it bypassed mutation detection (onChange,pendingCount). Usequery()for all reads and writes.
Server
- Correct delta routing — delta broadcast now uses the full geometry bounding box for intersection checks. Polygons and lines are correctly routed to all overlapping clients, not just those containing the first vertex.
- Ack message — server sends
{ type: "ack", write_ids: [...] }after each write batch is applied. - Write batch limit — batches larger than 500 edits are rejected.
- Real client IP — rate limiter reads
X-Forwarded-Forand strips the port fromRemoteAddr, so it works correctly behind a proxy. - WebSocket hardening — 1 MiB read limit, 60 s read deadline extended by pong, 30 s ping keepalive, write deadlines on all outgoing frames.
- Graceful shutdown —
SIGINT/SIGTERMtriggershttp.Server.Shutdownwith a 10 s drain. - Per-instance ServeMux — no longer registers on the global
DefaultServeMux. - Client UUID on connect — each WebSocket connection gets a server-generated UUID immediately, so log lines before the subscribe message are no longer blank.
- Dependency upgrade —
golang.org/x/crypto0.17.0 → 0.52.0.