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.

What each standard models as first-class, and what it has no place forSix concerns against how each standard handles them. STAC models asset links with roles and media types as first-class and has no model for custodianship or legal constraints. ISO 19115 models lineage, responsible party and constraints as first-class and handles asset links weakly. Spatial and temporal extent are first-class in both. The pattern is that STAC answers where the data is and how to fetch it, while ISO answers who is responsible for it and what may be done with it.STACISO 19115Asset linksfirst-classweakSpatial extentfirst-classfirst-classLineage detailvia extensionfirst-classResponsible partynot modelledrequiredLegal constraintsnot modelledfirst-classProgrammatic searchSTAC APICSW

2. Generate both from the manifest, never author either by hand

python
# 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:

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

bash
# 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

One manifest, two representations, and the divergence that generation preventsThe product manifest is the single source of truth. A generator emits the STAC item, which is the served record because services query it, and generates the ISO record on demand for institutional reporting. Both derive from the same fields, so they cannot disagree about extent, CRS or resolution. The rail records the alternative: two independently authored records, which diverge within two releases and leave nobody able to say which is correct.Product manifestsource of truthSTAC itemserved, machine-facingISO recordon demandgeneratedgeneratedauthored independentlyTwo records that disagreewithin two releases

4. Wire discovery to the standard consumers actually use

bash
# 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"'

Fitness-aware discovery in one query, because quality is mirrored into the recordA consumer issues one STAC API search combining a bounding box, a time interval and a maximum positional accuracy. The catalog matches the spatial and temporal filters and applies the accuracy property, returning only products fit for the consumer purpose. The final exchange records the alternative when quality lives in a separate endpoint: the consumer retrieves candidates, fetches quality for each, and filters client-side — several round trips for what should be one query.ConsumerSTAC APIQuality endpointbbox + datetime + rmse < 2.0fit products onlywithout mirroring: candidatesfetch quality per candidateN round trips

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.