Skip to content

Releases: a-saed/datum

v0.13.0 — MCP server

09 Jun 19:05

Choose a tag to compare

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 features

Or 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)

31 May 10:26

Choose a tag to compare

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.0

v0.11.1 — Gone notifications

31 May 09:27

Choose a tag to compare

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:

  • update operations that leave the predicate → gone notification sent
  • insert operations that never matched → still silently dropped (row was never in the client's DB)
  • delete operations → 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:latest

v0.11.0 — Non-spatial tables

30 May 22:55

Choose a tag to compare

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:latest

v0.10.0 — Per-delta RLS check

30 May 21:45

Choose a tag to compare

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:latest

v0.9.0 — Subscription predicates

30 May 17:23

Choose a tag to compare

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:

  1. Keyword blocklist — rejects SELECT, DROP, dblink, pg_read_file, etc. before touching the database
  2. EXPLAIN in a READ ONLY transaction — validates syntax and column references via Postgres's own parser
  3. pgx bound parameters — whereParams values 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.0
docker pull ghcr.io/a-saed/datum-server:latest

v0.8.0 — DevTools

30 May 01:16

Choose a tag to compare

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+Enter to 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 in DatumConfig.
  • 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.0
docker pull ghcr.io/a-saed/datum-server:latest

v0.7.1 — Schema cloning + typed column support

29 May 23:11

Choose a tag to compare

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. Query name, height, score, etc. with normal SQL on both sides.
  • properties JSONB 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.1
docker pull ghcr.io/a-saed/datum-server:latest

v0.6.0 — Per-user JWT authentication

29 May 01:20

Choose a tag to compare

What's new

Opt-in per-user authentication using JWT tokens.

Server

  • Configure auth.jwt_algorithm (HS256/RS256/ES256) and set JWT_SECRET env var or jwt_public_key file 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 auth wire message

Client

  • token?: string | () => Promise<string> in DatumClient.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 token config 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

28 May 21:40

Choose a tag to compare

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 seq column 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 sends updated_at with 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). Use query() 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-For and strips the port from RemoteAddr, 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 shutdownSIGINT/SIGTERM triggers http.Server.Shutdown with 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 upgradegolang.org/x/crypto 0.17.0 → 0.52.0.