STAC vs ISO 19115 for Mesh Catalogs
The two metadata standards are not alternatives so much as answers to different questions: one describes an artifact well enough for a machine to fetch it, the other describes a dataset well enough for an institution to be accountable for it. A federated mesh needs both, and choosing one exclusively produces either a catalog nobody can query programmatically or one no regulator will accept. This guide sets out what each covers, where they overlap, and how to publish one authoritative record that satisfies both. It is a format decision within Metadata Cataloging for Raster/Vector, inside Geospatial Data Mesh Fundamentals.
Prerequisites
| Requirement | Value / Assumption | Notes |
|---|---|---|
| Tools | python3 ≥ 3.11, pystac, xmllint, the catalog API |
Both formats validate mechanically |
| CRS convention | Extents normalized to EPSG:4326 for indexing |
Both standards expect geographic bounds |
| Source of truth | The product manifest | Both records are generated, never hand-edited |
| Access roles | gis-data-steward (institutional fields), domain-owner (technical) |
The split follows the standards |
| Environment | CATALOG_API, PRODUCT |
Exported before running |
Step-by-Step Implementation
1. Understand what each standard is actually for
| Concern | STAC | ISO 19115 |
|---|---|---|
| Unit described | An asset — a scene, a tile set, a file | A dataset or series |
| Primary reader | A machine fetching data | An institution or regulator |
| Spatial extent | GeoJSON geometry + bbox | EX_GeographicBoundingBox |
| Temporal | datetime or an interval |
EX_TemporalExtent |
| Asset links | First-class assets with roles and media types |
MD_DigitalTransferOptions, weaker |
| Lineage | Via extensions | LI_Lineage, first-class and detailed |
| Custodianship | Not modelled | CI_ResponsibleParty, required |
| Constraints | Not modelled | MD_LegalConstraints, first-class |
| Query | STAC API, spatial and temporal | CSW, richer text and thesaurus search |
| Extensibility | Extensions, widely used | Profiles, heavyweight |
The pattern is clear enough to act on: STAC answers where is the data and how do I fetch it; ISO answers who is responsible for it, how was it produced, and what may I do with it.
2. Generate both from the manifest, never author either by hand
# catalog_records.py — one source of truth, two published representations.
import json
def stac_item(manifest: dict, artifact: dict) -> dict:
"""The machine-facing record. Assets carry roles and media types so a client
can pick the right one without a human reading the description."""
m, s = manifest["metadata"], manifest["spec"]
return {
"stac_version": "1.0.0",
"type": "Feature",
"id": f"{m['domain']}-{m['name']}-{artifact['version']}",
"collection": f"{m['domain']}-{m['name']}",
"geometry": artifact["coverage_geojson"],
"bbox": artifact["bbox_4326"],
"properties": {
"datetime": artifact["materialized_at"],
"proj:code": s["storage"]["crs"],
"gsd": artifact.get("resolution_m"),
# Quality travels with the record, so fitness is visible at discovery.
"mesh:positional_accuracy_rmse_m": artifact["quality"].get("rmse_m"),
"mesh:geometry_validity_rate": artifact["quality"].get("validity_rate"),
},
"assets": {
"tiles": {"href": artifact["tiles_url"], "type": "application/vnd.mapbox-vector-tile",
"roles": ["data", "visual"]},
"snapshot": {"href": artifact["snapshot_url"], "type": "application/vnd.apache.parquet",
"roles": ["data"]},
},
"links": [{"rel": "license", "href": artifact["license_url"]}],
}
def iso_fields(manifest: dict, artifact: dict) -> dict:
"""The institution-facing record. Everything here has an accountable party."""
return {
"fileIdentifier": f"{manifest['metadata']['domain']}.{manifest['metadata']['name']}",
"responsibleParty": {"organisation": manifest["metadata"]["owner"], "role": "custodian"},
"lineage": {
"statement": artifact["lineage_statement"],
"processStep": [
{"description": f"reprojected {artifact['source_crs']} -> "
f"{manifest['spec']['storage']['crs']}",
"rationale": artifact["transformation_pipeline"]},
{"description": "topology validated", "rationale": "ST_IsValid, blocking gate"},
],
},
"legalConstraints": artifact["constraints"],
"spatialResolution": artifact.get("resolution_m"),
"referenceSystem": manifest["spec"]["storage"]["crs"],
}
Verify both records validate and agree on the facts they share:
python3 -c "
import json, catalog_records, pystac
item = pystac.Item.from_dict(json.load(open('/tmp/stac_item.json')))
item.validate(); print('STAC valid')"
xmllint --noout --schema gmd.xsd /tmp/iso_record.xml && echo "ISO valid"
# The shared facts must match: extent, CRS, resolution.
diff <(jq -c '.bbox' /tmp/stac_item.json) <(xmllint --xpath '//gmd:EX_GeographicBoundingBox' /tmp/iso_record.xml)
3. Publish one and derive the other
Publishing both as independently maintained records guarantees they diverge. Pick the one your primary consumers query and derive the other on demand.
# STAC is the served record for a mesh whose consumers are machines; the ISO record
# is generated on request for institutional reporting, from the same manifest.
curl -sS -X PUT "$CATALOG_API/products/$PRODUCT/stac" \
-H 'content-type: application/geo+json' --data-binary @/tmp/stac_item.json
curl -sS "$CATALOG_API/products/$PRODUCT/iso19115" -H 'accept: application/xml' \
| xmllint --format - | head -20
4. Wire discovery to the standard consumers actually use
# STAC API search: spatial, temporal, and quality-aware in one query.
curl -sS -X POST "$CATALOG_API/search" -H 'content-type: application/json' -d '{
"bbox": [-1, 50, 1, 52],
"datetime": "2026-01-01T00:00:00Z/..",
"query": {"mesh:positional_accuracy_rmse_m": {"lt": 2.0}},
"limit": 20
}' | jq -r '.features[] | "\(.id) rmse=\(.properties["mesh:positional_accuracy_rmse_m"])m"'
Configuration Reference
| Decision | Choose STAC when | Choose ISO when |
|---|---|---|
| Primary consumer | Services and pipelines | Institutions and regulators |
| Discovery pattern | Spatial/temporal search | Thesaurus and text search |
| Lineage depth | A statement suffices | Process steps are required |
| Custodianship | Implicit in domain ownership | Must be a named responsible party |
| Constraints | Handled by access policy | Must be in the metadata record |
| Extension effort | Low — extensions are routine | High — profiles are heavyweight |
| Recommendation | Serve STAC | Generate ISO on demand |
Common Failure Modes & Fixes
The two records disagree about the extent. Root cause: both hand-maintained. Fix: generate both from the manifest; a record authored twice will diverge, invariably within two releases.
ISO validation fails on a required responsible party. Root cause: the mesh models ownership as a domain rather than a named organisation, which ISO does not accept. Fix: map the owning team to an organisation identity in the generator; the mapping belongs in the domain registration, not in each record.
STAC search returns nothing despite matching data.
Root cause: the item’s geometry is in a projected CRS. STAC requires EPSG:4326 for geometry and bbox regardless of the asset’s own CRS. Fix: normalize at generation; keep the native CRS in proj:code.
Quality metrics are absent from discovery. Root cause: they live in a separate quality endpoint, so a consumer cannot filter on fitness while searching. Fix: mirror the key indicators into STAC properties under a namespaced prefix, as above.
An ISO record is required and takes a week to produce. Root cause: it is being authored rather than generated. Fix: generation from the manifest turns a week into a request; the fields ISO needs and the mesh does not — custodian, constraints, lineage statement — belong in the manifest so they exist before they are asked for.
FAQ
Can a mesh get away with STAC alone?
For technical consumers, comfortably. For an estate with regulatory obligations, no — and the gap is not cosmetic. ISO models custodianship, legal constraints and detailed lineage as first-class required elements precisely because those are what an institution is accountable for, and STAC has no equivalent because it was designed for a different job. The workable position is that STAC is the served record because it is what services query, and the ISO record is generated on demand from the same manifest whenever an institutional obligation calls for it.
Which should the routing plane read?
Neither, directly. The routing plane resolves ownership from the product registry’s declared coverage, which is a smaller and more tightly controlled thing than either catalog record — it needs an extent, a layer, and a domain, evaluated cheaply on every request. Catalog records are for discovery by humans and by consumers choosing a product; making routing depend on a full metadata document puts a large parse on the hot path for no benefit.
How do the quality indicators fit?
Under a namespaced STAC property prefix, mirrored from the product’s quality block. The value of putting them there rather than leaving them in a separate endpoint is that discovery becomes fitness-aware: a consumer can search for products covering their extent with accuracy better than two metres in one query, instead of retrieving candidates and then fetching quality for each. ISO carries the same information in its data-quality section, generated from the same source.
Does adopting both double the maintenance?
Only if both are authored. Generated from one manifest, the marginal cost of the second representation is the generator itself, which is written once and then produces records that cannot disagree. The genuine cost is that the manifest must carry the union of both standards’ required fields — custodian, constraints, lineage statement — which is a handful of extra fields at domain registration and is worth having regardless.
How do the two standards handle a product with many versions?
Differently, and the difference is worth planning around. STAC models an item per artifact within a collection, so a versioned product maps naturally onto a collection whose items are its materializations — discovery by extent and time works without any extension. ISO models a dataset or a series, and version history sits inside the record rather than as separate records, which suits an institutional view of “this dataset, its lineage and its custodian” and suits programmatic version selection badly. Serving STAC for discovery and generating ISO per published version keeps both honest.
Should the catalog record link back to the charter?
Yes, and it is the cheapest usability improvement available. A consumer discovering a product through a spatial search has found something that matches their extent and their accuracy requirement, and still does not know whether it is for their use case — which is exactly what the charter’s purpose and exclusions answer. A link from the catalog record to the charter turns a discovery result into a decision, and it costs one field.
Related
- Metadata Cataloging for Raster/Vector — the parent topic and the extent-precision rules
- Best Practices for Spatial Metadata Catalogs — the ingestion gate both records are generated behind
- Spatial Data Quality and Validation Standards — the indicators mirrored into discovery