Spatial Data Versioning and Rollback Workflows

When a bad dataset publish reaches production — the wrong CRS, truncated features, a broken attribute schema — an operations team without a versioning discipline has no clean way back. This guide describes how to version spatial datasets and their metadata together so a faulty publish can be reversed by repointing an alias, not by frantically re-loading data under pressure.

This guide sits within the Metadata Catalog Automation & Ingestion Workflows practice, downstream of the validation gate that should have caught the bad batch and alongside the sync mechanisms that keep regional nodes consistent. It assumes datasets are published continuously by automated pipelines, that catalog records must stay truthful about what is actually served, and that the mean time to recovery from a bad publish is a number your users measure whether or not you do.

A publish is never an in-place mutation of the live store. It is an append of a new immutable version behind an alias, followed by an atomic pointer swap; rollback is the same swap in reverse.

Publish and rollback state machine for versioned spatial datasets An alias points at the current dataset version. A publish job writes a new immutable candidate version to a separate versioned store and validates it while the alias still serves the old version. On validation pass the alias is atomically repointed to the candidate, which becomes current, and the prior version is retained rather than deleted. If a post-publish check or a user report signals a bad publish, the alias is atomically repointed back to the retained prior version and the bad candidate is quarantined for inspection. Cache invalidation fires on every alias change so clients never mix versions. v1 · current alias -> v1 store v2 · candidate written to new store alias untouched validate CRS · count · schema v2 · current alias -> v2 store v1 retained v1 · retained last-good, warm rollback target v2 · quarantined held for post-incident review publish pass · swap alias demote rollback · swap back fail

Where versioning sits in the publish path

Versioning is a property of the serving layer, and it must be established before the first byte of a dataset reaches a client. The pipeline should treat a publish as three separable acts: writing an immutable version to storage, validating that version in isolation, and promoting it by swapping a stable alias. Only the third act is visible to consumers, and only the third act is what a rollback undoes. Teams that collapse these into a single “load into the live table” step have no rollback primitive at all — the previous state is already overwritten by the time anyone notices the CRS is wrong.

The alias is the load-bearing abstraction. Every downstream consumer — the WMS/WFS endpoint, the tile pyramid, the catalog record’s distribution URL — resolves the dataset through a stable name (parcels, say) that never changes, while the physical store behind it (parcels_v2026_07_13) is versioned and disposable. GeoServer models this cleanly: a published layer references a datastore, and the datastore can be repointed without changing the layer’s name or its capabilities document. That indirection is what lets a rollback be an O(1) pointer move rather than an O(n) data reload.

Because the same alias indirection is what keeps regional nodes agreeing on which version is authoritative, versioning is tightly coupled to synchronization. The tagging conventions and cross-node reconciliation in version tagging & sync for spatial datasets are the mechanism that propagates an alias swap consistently; this guide covers the local state machine, that companion page covers making it converge across a fleet.

Two versioning models: immutable snapshots vs event-sourced changesets

There are two defensible ways to version a spatial dataset, and the choice determines how expensive a rollback is and how much history you retain.

Immutable snapshots treat each publish as a complete, self-contained copy of the dataset at a point in time, written to a new store and never mutated afterward. Rollback is trivial — repoint the alias at the prior snapshot — and reasoning is simple because every version is a full, queryable table. The cost is storage: a 40 GB parcel layer republished nightly is 40 GB per retained version. Snapshots suit datasets that are republished wholesale (a nightly export from an authoritative source) and where the retention window is short.

Event-sourced changesets store an immutable log of feature-level operations (insert, update, delete, geometry-correct) and materialize any version by replaying the log to a target revision. Storage is proportional to change volume, not dataset size, so long histories are cheap, and you get a precise audit of who changed which feature when. The cost is complexity: materializing a version is compute, replay must be deterministic, and point-in-time queries need careful indexing. Changesets suit continuously-edited datasets (a live cadastre, an incident feed) where most publishes touch a small fraction of features.

Concern Immutable snapshots Event-sourced changesets
Rollback cost O(1) alias swap to a warm store Replay log to prior revision, then swap
Storage cost Full copy per version Proportional to change volume
Audit granularity Dataset-level Per-feature, per-operation
Point-in-time query Query the snapshot directly Materialize the revision first
Best fit Wholesale nightly republish Continuously edited layers

Many production catalogs run a hybrid: event-sourced changesets as the system of record for edits, with periodic materialized snapshots pinned as named checkpoints so rollback to a checkpoint stays O(1) while fine-grained history stays cheap. Whichever model you pick, the version identifier must be immutable once assigned, because catalog records, tile caches, and audit logs will all reference it by name.

Layer and version tagging conventions

A version tag is a contract between the data store, the serving layer, and the catalog. It must be sortable, collision-free, and carry enough provenance to answer “what is this and where did it come from” without a database lookup. A workable convention pins a monotonic revision, an ISO date, and the source content hash.

# dataset-version.yaml — the manifest written alongside every published version.
# Immutable once the version is promoted; the catalog record references version.id.
apiVersion: catalog.publish/v1
dataset: parcels
version:
  id: parcels@2026-07-13T02-15-00Z.r418        # alias@timestamp.revision — sortable, unique
  revision: 418                                 # monotonic counter, never reused
  store: parcels_v2026_07_13_r418               # physical datastore / schema name
  source_hash: "sha256:7f3c1a9e4b2d..."         # content hash of the source extract
  crs: "EPSG:27700"                             # declared, verified against features
  feature_count: 1284533                        # verified post-load, gate rejects drift
  bbox: [-7.6, 49.9, 1.8, 55.9]                 # WGS84 envelope for sanity checks
provenance:
  source: "os-open-data-nightly"
  published_by: "publish-pipeline"
  parent_version: parcels@2026-07-12T02-14-00Z.r417   # the version this supersedes
status: candidate                               # candidate -> current -> retained -> archived

The store name embeds the revision so two versions can coexist physically during a publish, which is precisely what makes the swap atomic. The parent_version pointer turns the set of versions into a linked history you can walk backward — the rollback procedure simply follows it to find the last version whose status was current and passed validation. Never encode the tag as a mutable label like latest on the physical store; latest belongs only on the alias, resolved at serve time.

Atomic publish via blue/green layer swap

The safest publish is a blue/green swap: the new version is fully written and validated in a store that no client is reading, and promotion is a single repoint of the alias. Because the swap touches only a pointer, it is atomic from the client’s perspective — a GetMap request resolves entirely against the old store or entirely against the new one, never a half-loaded table.

Concretely, publish to a new versioned datastore, validate it out-of-band, then repoint the layer’s datastore reference through the GeoServer REST API:

# 1. Register the new versioned datastore (blue -> green candidate).
curl -sf -u "$GS_USER:$GS_PASS" -XPOST \
  -H "Content-Type: application/json" \
  "$GS_URL/rest/workspaces/geoportal/datastores" \
  -d '{"dataStore":{"name":"parcels_v2026_07_13_r418",
        "connectionParameters":{"entry":[
          {"@key":"host","$":"postgis"},
          {"@key":"database","$":"geoportal"},
          {"@key":"schema","$":"parcels_v2026_07_13_r418"}]}}}'

# 2. Publish the layer against the candidate store, then validate it out-of-band
#    (feature count, CRS, schema) BEFORE touching the alias.

# 3. Atomic promote: repoint the published alias layer at the candidate store.
curl -sf -u "$GS_USER:$GS_PASS" -XPUT \
  -H "Content-Type: application/json" \
  "$GS_URL/rest/layers/geoportal:parcels" \
  -d '{"layer":{"defaultStyle":{"name":"parcels"},
        "resource":{"@class":"featureType",
        "name":"parcels_v2026_07_13_r418:parcels"}}}'

The alias layer geoportal:parcels keeps its name, its style, and its capabilities entry across the swap, so cached client bookmarks and the catalog’s distribution URL never break. The OGC service contract — the WMS and WFS interfaces clients bind to — is defined against the alias, not the physical store, which is exactly why the swap is invisible to conformant clients. Keep the previous datastore registered and warm after promotion; it is your rollback target, and re-registering a deleted store is far slower than repointing to one that is still there.

For the tile tier the same principle applies, but the unit of atomicity is the cache. A tile pyramid seeded from parcels_v417 must be invalidated or re-seeded when the alias moves to r418, or clients will render new vectors over stale raster tiles. The child procedure below and the seeding guidance in tuning MapProxy cache seeding for large extents cover making that re-seed bounded rather than a full-planet reflow.

Coupling dataset versions to catalog records

A rollback that moves the data but leaves the catalog describing the withdrawn version is only half a rollback — discovery clients will still advertise the wrong CRS, the wrong extent, or a broken schema. Data and metadata must roll back together, which means the catalog record has to reference the version, not just the dataset name.

Bind the CSW record’s identifier and distribution section to the version id from the manifest, and treat a catalog update as part of the same promotion transaction as the alias swap. When PostGIS is your metadata store as well as your feature store, wrap the record repoint and the version-status flip in one transaction so they commit or abort together:

-- Promote a version and its catalog record atomically in PostgreSQL/PostGIS.
BEGIN;
  -- flip the catalog record's live pointer to the new version
  UPDATE catalog.records
     SET current_version = 'parcels@2026-07-13T02-15-00Z.r418',
         distribution_url = 'https://geo.example.gov/geoserver/geoportal/wms',
         updated_at = now()
   WHERE dataset = 'parcels';
  -- demote the prior version, promote the candidate
  UPDATE catalog.versions SET status = 'retained' WHERE status = 'current' AND dataset = 'parcels';
  UPDATE catalog.versions SET status = 'current'  WHERE id = 'parcels@2026-07-13T02-15-00Z.r418';
COMMIT;

Because PostgreSQL DDL and row updates inside a BEGIN/COMMIT are transactional, a failure at any point leaves both the record and the version-status table on the old version — there is no window where the catalog advertises a version the alias is not serving. The upstream mapping-and-validation gate in CSW catalog schema mapping & validation is what guarantees the metadata attached to each version was itself well-formed before it was ever eligible to be promoted, so a rollback restores a record you can trust.

Retention, audit, and cache invalidation on version change

Retention policy decides how far back you can roll. Keep at least the last N current versions warm (registered and queryable) so any of them is an O(1) rollback target, and archive older versions to cold object storage where restoring is possible but slower. A common shape is: keep the three most recent versions warm, retain thirty days of daily versions in cheap storage, and pin named checkpoints (a quarterly authoritative release) indefinitely. Never let retention drop below one prior version — a dataset with only its current version has no rollback target at all.

Every state transition is an audit event: who published, which source hash, what validation verdict, when the alias moved, and — critically — every rollback with its trigger and the operator who authorized it. This log is what a post-incident review reads, and what change-detection reconciles against; the delta-tracking discipline in incremental metadata harvesting and change detection consumes these same version transitions to decide what actually changed between harvest cycles.

Cache invalidation must be driven by the alias change, not scheduled on a timer. The moment the alias moves, fire a targeted purge: the WMS/WMTS tile cache for the affected layer and extent, any CDN edge holding GetMap/GetTile responses, and the client-visible ETag/Cache-Control validators on capabilities documents. Scope the purge to the changed layer and, where the versioning model knows it, to the changed extent — a changeset publish that edited one county should not invalidate a continent of tiles. On rollback the same purge fires in reverse, because tiles seeded from the bad version are exactly as wrong as the features were.

Operational troubleshooting

Diagnose versioning and rollback failures by symptom, the surface that reveals the cause, and the fix:

Symptom Likely cause Where to look Fix
Rollback swaps data but clients see old attributes Catalog record still points at withdrawn version catalog.records.current_version vs the live alias Repoint the CSW record in the same transaction as the alias swap
New vectors render over stale tiles after publish Cache not invalidated on alias change Tile cache seed timestamp vs alias-change event Fire a scoped purge/re-seed keyed on the alias change, not a timer
No warm store to roll back to Retention deleted the prior version Version manifest status, storage inventory Keep ≥ N recent versions warm with reclaimPolicy: Retain-style retention
Publish leaves a half-loaded live table In-place load instead of blue/green Publish job — does it write a new store or mutate the live one Always write to a new versioned store; promote only by alias swap
Alias swap succeeds but capabilities unchanged Layer renamed instead of repointed GeoServer layer name before/after swap Keep the alias layer name stable; change only its resource/datastore reference
Rollback target has wrong feature count Prior version was itself never validated Version manifest feature_count vs live count Only mark a version current/retainable after it passes the validation gate
Regions disagree on which version is live Alias swap not propagated across nodes Per-node current-version tag vs registry Reconcile via the version-tagging sync so all nodes converge on one alias target

Treat rollback as a rehearsed, routine operation, not an emergency improvisation — the concrete keystroke-level procedure lives in the companion page below, and the difference between a two-minute recovery and a two-hour outage is almost always whether the prior version was left warm and the cache purge was scoped in advance.

Up one level: Metadata Catalog Automation & Ingestion Workflows.