Short answer
Versioning GIS data well means three habits: semantic version numbers, immutable releases (never overwrite a published file), and a reproducible pipeline where the source extract, processing code, and parameters are versioned alongside the output. Plain git handles code and small text-based spatial files; large GeoPackages and rasters need a data-versioning layer such as DVC or git LFS that keeps the binary in object storage and tracks a checksum in git. The payoff is that anyone can ask "which dataset produced figure 7" and get a definitive, checkable answer.
This article gives the versioning scheme, the tooling for spatial data specifically, a reproducible export pattern from PostGIS, and the validation that keeps versions honest.
Why GIS data resists ordinary versioning
GIS data breaks the assumptions behind code version control:
- Files are large and binary. A national GeoPackage or a lidar DEM is hundreds of megabytes to gigabytes. Committing it to git bloats the repository irreversibly, because git stores full history.
- Small edits create huge diffs. Re-saving a binary GeoPackage after editing one polygon rewrites the whole file; git sees a complete change, not a one-feature edit.
- State lives in a database. PostGIS data is not a file at all. Versioning means snapshotting query results or migrations, not tracking a file on disk.
So GIS versioning is less about diffing files and more about identifying and reproducing states: which extract, which code, which parameters produced this artifact.
The versioning scheme
Semantic versions with predictable meaning
Adopt MAJOR.MINOR.PATCH and define what each bump means for a data consumer:
- MAJOR — schema or methodology change. Field renamed, CRS changed, classification redefined. This may break a consumer's joins or styling, so it is a loud signal.
- MINOR — data added or extended within the same schema. New survey area, more features. Backward-compatible.
- PATCH — corrections that do not change the schema. A fixed geometry, a corrected attribute.
A consumer reading 2.3.1 → 3.0.0 knows to expect breakage; 2.3.1 → 2.4.0 they can usually drop in.
Immutable releases
Never overwrite a released artifact. Publish each version to its own path — corridor/2.3.0/corridor.gpkg — so a citation made against 2.3.0 resolves forever. A single mutable corridor_latest.gpkg makes every past reference ambiguous: nobody can tell which data a six-month-old report actually used. "Latest" may exist as a pointer, but never as the only copy.
Checksums and a changelog
Compute a SHA-256 per published file (sha256sum corridor.gpkg) and record it. The checksum answers "is this byte-for-byte the file you delivered." A human-readable changelog answers "what changed between versions and why."
Tooling for spatial data
git for code and small text data
Use git for processing scripts, SQL migrations, configuration, and small text-based spatial files like GeoJSON and CSV where line-based diffs are meaningful. Keep large binaries out.
A data-versioning layer for large files
For GeoPackages, GeoTIFFs, and DEMs, add one of:
- DVC (Data Version Control) — stores the large file in object storage (S3, GCS, Azure) and commits a small
.dvcpointer with a hash to git.dvc add dem.tifthengit add dem.tif.dvc;dvc pullretrieves the exact version for a checked-out commit. - git LFS — replaces large files with text pointers, storing the blobs on an LFS server. Simpler, but less suited to data-pipeline workflows than DVC.
- git-annex — similar pointer model with flexible backends.
All three give you the same property: the git history stays small and a commit unambiguously identifies a dataset version by hash.
Database versioning
For PostGIS, version the schema with migration tools (Flyway, sqitch, or plain numbered SQL scripts in git) and version the data state by tagging extracts. The reproducible-export pattern below turns a database state into a versioned, hashable file.
Reproducible export from PostGIS
Outputs should be generated by a script, never by hand-clicking through export menus, so a version can be regenerated on demand.
#!/usr/bin/env bash
set -euo pipefail
VERSION="2.3.0"
OUT="corridor/${VERSION}"
mkdir -p "$OUT"
# deterministic export: fixed query, explicit CRS, sorted by id
ogr2ogr -f GPKG "$OUT/corridor.gpkg" \
PG:"host=db dbname=project user=reader" \
-sql "SELECT id, name, status, geom FROM corridor_v2 ORDER BY id" \
-nln corridor -a_srs EPSG:2154
sha256sum "$OUT/corridor.gpkg" > "$OUT/corridor.gpkg.sha256"
ORDER BY id makes feature order deterministic, which keeps the checksum stable across re-runs. Commit the script and the .sha256; track the GeoPackage with DVC. Given the tag 2.3.0, the source table version, the script, and the parameters are all recoverable, so the output can be rebuilt and verified against the recorded hash.
The version record
Attach a sidecar record to every release so versioning is self-describing:
{
"dataset": "corridor",
"version": "2.3.0",
"crs": "EPSG:2154",
"source": "ACME survey 2026-01; extract 2026-02-02",
"processing": "buffer 25 m, dissolve by status; script v2.3.0",
"licence": "Client-confidential",
"sha256": "a31f…",
"changelog": "Added west extension; corrected segment 4 status"
}
This is exactly the information that answers an audit question months later without archaeology.
Worked example: shipping a corrected version
- Field correction changes one segment's status. Apply it as a migration in git, not a manual edit, and re-run the pipeline.
- Schema unchanged, data corrected → bump PATCH: 2.3.0 → 2.3.1.
- The export script writes
corridor/2.3.1/corridor.gpkgand its.sha256; DVC stores the binary, git commits the pointer. - Append the changelog; update the version record.
- 2.3.0 still resolves for anyone who cited it; 2.3.1 is the new default.
Common pitfalls and why they happen
- Overwriting "latest". A single mutable file destroys traceability; old citations become unverifiable. The fix is immutable versioned paths.
- Committing big GeoPackages to git. The repo bloats permanently because git keeps full binary history. Use DVC/LFS and keep blobs in object storage.
- Manual exports. Hand-clicked exports cannot be reproduced and drift between runs. Script them with deterministic ordering.
- Versioning the output but not the source. Without the source extract and parameters, a version cannot be rebuilt or defended. Version source, code, and output together.
- No checksum. Without a hash you cannot prove a file is the one you sent. Record SHA-256 per artifact.
Validation
- Re-run the export script for a tagged version and confirm the output matches the recorded SHA-256 (or is feature-identical if non-deterministic).
- Confirm every released path is immutable and that "latest" only points, never replaces.
- Check that each version record states CRS, source, parameters, licence, and changelog.
- Verify a clean checkout plus
dvc pullretrieves exactly the dataset version the commit references.
Bathyl perspective
We version geospatial work so that a study stays auditable long after delivery. Source, code, parameters, and output move together under one version tag, and immutable releases mean a figure can always be traced to the exact data behind it. That traceability is what turns a one-off deliverable into a durable, defensible asset.
Related reading
- Reproducible Spatial Analysis Products
- From Consulting Output to Data Product
- Data Delivery Portals for Consultants
- Spatial data products