Self-Serve Platform Capabilities for Spatial Teams

The platform’s job is to make the correct way to publish a spatial product the easiest way, and every capability it offers should be judged against that.

Domain ownership only produces autonomy if a domain team can actually stand a product up without waiting on anyone. Where it cannot — where provisioning PostGIS means a ticket, where publishing a tile endpoint means a platform engineer’s afternoon, where wiring metrics means reading three internal wikis — federation has moved the coordination cost rather than removed it, and domains quietly revert to whatever central system still works. This topic, inside Geospatial Data Mesh Fundamentals, specifies the capability set a spatial platform has to provide, how those capabilities are exposed, and how to tell a platform that enables autonomy from one that merely relocates the bottleneck. It is the practical counterpart to the ownership model in Spatial Domain Boundary Design: boundaries without a platform produce twelve teams each rebuilding a reprojection pipeline.

Architectural Boundaries & Design Rationale

A self-serve platform is defined by what it refuses to do as much as by what it offers. Three boundaries decide whether it stays a platform or becomes a new monolith.

The platform owns capability, domains own data. The platform provides the machinery to reproject, validate, tile, publish and monitor; it never holds a domain’s data, never runs a domain’s business logic, and never has a schema of its own that domains must conform to. The moment the platform holds a shared table that domains write into, every schema change is a coordination event again and the mesh is over.

The platform is opt-out, not mandatory. A capability a domain cannot decline is not a platform capability; it is a policy. Domains must be able to substitute their own implementation — their own orchestrator, their own tile renderer — provided the output port and its contract are unchanged. This constraint is what keeps the platform honest: a capability teams would abandon if allowed to is a capability that needs improving, and mandating it merely hides that signal.

The platform’s interface is a declaration, not an API call sequence. A domain declares what it wants — this product, this CRS, this resolution, these ports — and the platform reconciles reality toward that declaration. It does not offer a create endpoint, an update endpoint, and a delete endpoint that a domain must orchestrate correctly. Declarative interfaces are idempotent by construction, which means a partially failed provisioning is fixed by re-applying rather than by reasoning about what already happened.

The load-bearing consequence of all three is that the platform must be usable without understanding it. A domain engineer publishing their first spatial product should not need to know how the tile cache invalidates, how the catalog indexes extents, or which reprojection library runs underneath. They should need to know their data, their CRS, and their consumers. Every unit of platform internals a domain must learn is a unit of coupling that will resist change later, which is why platform capability is measured in time-to-first-published-product rather than in feature count.

The capability stack, with the three layers that have no escape hatchFive capability layers read bottom to top. Provisioning, storage and processing capabilities can all be substituted by a domain that brings its own implementation, provided the output port and contract are unchanged. Catalog registration, access control and cost attribution sit at the top and have no escape hatch, because their consequences land on the estate rather than on the domain that declines them: an unregistered product is unroutable, an unenforced policy is a hole in the perimeter, and unattributed cost is a bill somebody else pays.Catalog · access · costno escape hatchconsequences land on the estateObservabilityown exporter, same metric namessubstitutableProcessingreproject · validate · tilesubstitutableStoragePostGIS or object prefixsubstitutableProvisioningstore · credentials · namespacesubstitutable

Specification & Contract Reference

The capability set below is the minimum for a spatial mesh. Each is expressed as a declaration a domain makes, and each carries an explicit escape hatch.

Capability Domain declares Platform provides Escape hatch
Domain provisioning Domain id, perimeter, owning team Isolated store, credentials, namespace Bring your own store, register the port
Storage Storage class, retention, CRS PostGIS instance or object prefix Self-managed backend
Reprojection Source and target CRS, transformation Pinned PROJ pipeline + grid package Own warp step, declare the pipeline
Validation Indicator set, blocking thresholds Quality gate, report, catalog write Own gate, publish the same indicators
Tiling Matrix set, zoom range, format Tile pyramid build and publish Own renderer behind the same port
Output ports Port types, versions Endpoint, TLS, routing registration Own endpoint, register with the router
Catalog registration Manifest Extent normalization, index, discovery None — registration is mandatory
Observability SLO targets Metrics, traces, dashboards, alert routing Own exporter, same metric names
Access control Allowed principals, spatial scope mTLS identity, policy enforcement None — enforcement is mandatory
Cost attribution Per-consumer serving cost None — attribution is automatic

Three capabilities have no escape hatch, and the pattern is deliberate: catalog registration, access control, and cost attribution are the ones where an opt-out would externalise cost onto everybody else. An unregistered product is invisible and therefore unroutable; an unenforced access policy is a hole in the estate’s perimeter; unattributed cost is a bill somebody else pays. Everything a domain can absorb the consequences of alone is optional; everything whose consequences land on the mesh is not.

The declaration itself is a single manifest, and its shape is the platform’s real interface.

yaml
# product.yaml — the whole declaration a domain makes to publish a spatial product.
apiVersion: mesh.geospatial/v1
kind: SpatialProduct
metadata:
  domain: cadastral
  name: parcels
  owner: team-cadastral
spec:
  storage:
    class: postgis
    crs: "EPSG:4326"                 # canonical storage CRS
    retention: P7Y
  source:
    crs: "EPSG:32633"                # declared, never inferred
    transformation: "NTv2:national-grid-2024"   # pinned, not auto-selected
  quality:
    blocking: [geometry_validity_rate, crs_conformance, coordinate_precision]
    precision_bound: 6
  ports:
    - type: tiles
      matrixSet: WebMercatorQuad
      zoom: { min: 6, max: 18 }
    - type: features                  # OGC API – Features
    - type: snapshot
      format: geoparquet
      cadence: P1D
  slo:
    availability: 0.9995
    latency_p95_ms: 300
    freshness: P1D
  access:
    principals: ["logistics/*", "planning/reporting"]
    spatialScope: extent              # scope-limited, not estate-wide

Production Implementation

The platform reconciles that declaration. The reconciler is idempotent by construction: it computes desired state from the manifest, compares it against observed state, and applies only the difference — so re-running after a partial failure completes the work rather than duplicating it.

python
# reconcile.py — declarative provisioning for a spatial product.
# Idempotent: applying the same manifest twice is a no-op, which is what makes a
# half-failed provisioning recoverable by re-applying rather than by unwinding.
import hashlib
import subprocess
import yaml

STEPS = ("store", "transformation", "quality_gate", "ports", "catalog", "policy", "slo")


def desired_state(manifest: dict) -> dict:
    """Everything the manifest implies, keyed so drift is detectable per step."""
    spec = manifest["spec"]
    domain = manifest["metadata"]["domain"]
    name = manifest["metadata"]["name"]
    return {
        "store": {"class": spec["storage"]["class"], "crs": spec["storage"]["crs"],
                  "namespace": f"{domain}-{name}"},
        "transformation": {"pipeline": spec["source"]["transformation"]},
        "quality_gate": {"blocking": sorted(spec["quality"]["blocking"]),
                         "precision": spec["quality"]["precision_bound"]},
        "ports": sorted(p["type"] for p in spec["ports"]),
        "catalog": {"domain": domain, "product": name},
        "policy": {"principals": sorted(spec["access"]["principals"]),
                   "scope": spec["access"]["spatialScope"]},
        "slo": spec["slo"],
    }


def fingerprint(step_state: dict) -> str:
    """Stable digest of one step's desired state — the drift comparison key."""
    canonical = yaml.safe_dump(step_state, sort_keys=True).encode()
    return hashlib.sha256(canonical).hexdigest()[:16]


def reconcile(manifest_path: str, dry_run: bool = False) -> int:
    manifest = yaml.safe_load(open(manifest_path))
    desired = desired_state(manifest)
    changed = 0
    for step in STEPS:
        want = fingerprint(desired[step])
        have = subprocess.run(
            ["mesh-platform", "get-fingerprint", step, manifest["metadata"]["name"]],
            capture_output=True, text=True,
        ).stdout.strip()
        if want == have:
            continue                      # already reconciled — do nothing
        changed += 1
        if dry_run:
            print(f"would apply: {step} ({have or 'absent'} -> {want})")
            continue
        subprocess.run(
            ["mesh-platform", "apply", step, "--manifest", manifest_path],
            check=True,                   # fail closed: a failed step stops the run
        )
    return changed

Declarative reconciliation: compare fingerprints, apply only the differenceA domain applies a manifest. The reconciler computes the desired state for each step and its fingerprint, asks the platform what the observed fingerprint is, and applies only the steps that differ. Steps already reconciled are skipped entirely. A second application of the same manifest therefore reports zero changes, which is what makes a half-failed provisioning recoverable by re-applying rather than by reasoning about what already happened. A non-zero count on an unchanged manifest means a step is computing its fingerprint non-deterministically.DomainReconcilerPlatformapply manifestobserved fingerprint per stepstore: match · policy: differsapply policy onlyapply again → 0 changesidempotent by construction

Applying it, and confirming that a second application is genuinely a no-op:

bash
# First apply provisions everything the manifest implies.
mesh-platform apply --manifest product.yaml

# Re-apply: the reconciler should report zero changes. A non-zero count on an
# unchanged manifest means a step is not computing its fingerprint deterministically.
mesh-platform apply --manifest product.yaml --dry-run
# expected: "0 changes"

# Confirm the product is discoverable and its ports resolve.
curl -s "$CATALOG_API/products/cadastral/parcels" | jq '{version, ports: [.ports[].type], slo}'

Diagnostic Runbook

  1. When a domain reports “provisioning is stuck”, re-apply before investigating. The reconciler is idempotent, so re-applying is free and completes any step that failed transiently. Only a step that fails twice is a real fault.
  2. Read the per-step fingerprints, not the logs. mesh-platform get-fingerprint <step> <product> shows exactly which step’s observed state diverges from the manifest. A stuck provisioning almost always has one divergent step, and the logs contain every step.
  3. A port that provisions but never serves usually means routing registration, not the port itself. Confirm the router’s compiled table contains the product before debugging the backend; a port with no route is healthy and unreachable.
  4. If the same manifest yields different results in two environments, compare the pinned transformation and grid package first. This is the most common environment-specific divergence in a spatial platform, and it produces subtly different coordinates rather than an error.
  5. A domain that has bypassed a capability should be visible, not hidden. Query the catalog for products whose declared ports do not match platform-provisioned ports. A domain running its own renderer is fine; a domain running its own renderer that nobody knows about is an outage waiting to be misdiagnosed.
  6. When time-to-first-product regresses, instrument the path rather than surveying the team. Record timestamps at manifest submission, first successful reconcile, and first successful publish. The gap that grew names the capability that needs work.
  7. For a capability nobody uses, ask whether it is undiscoverable or unnecessary before improving it. Both look identical in usage metrics, and only one is worth engineering effort.

The Golden Path, and Why It Must Stay Narrow

A capability list is not yet a platform. What turns it into one is a golden path: a single, opinionated, documented route from “we have some spatial data” to “it is published, discoverable, monitored and governed”, which works end to end without a decision the team is not equipped to make.

The golden path’s value comes from being narrow. Every option it offers is a decision a new domain must research, and a path with twelve choices at each step is a research project rather than a route. The correct default for a spatial mesh is opinionated to the point of being slightly uncomfortable: storage in PostGIS at EPSG:4326, tiles in WebMercatorQuad, snapshots in GeoParquet, blocking quality gates on validity and precision, an availability SLO of 99.9%. A domain with a genuine reason to differ can differ — the escape hatches exist — but it has to have the reason, and needing one is the point.

Golden-path step Opinionated default When a domain should deviate
Storage CRS EPSG:4326 Survey-grade metric work inside one UTM zone
Tile matrix set WebMercatorQuad A polar or national grid the consumers already use
Snapshot format GeoParquet Raster products, which take COG instead
Blocking gates Validity, CRS, precision Never — these are the floor
Availability SLO 99.9% A product whose consumers genuinely need more, or less
Update cadence Daily Source cadence differs materially

Two properties keep the path honest over time. It must be executable, not documented: a page describing the steps decays, while a template repository that provisions a working product on first run cannot decay without failing visibly. And it must be the path the platform team themselves use to stand up their own reference products, because a golden path its authors bypass is a path that has stopped matching reality and nobody has noticed.

Time to first published product, and what a regression is telling youElapsed hours from a new domain cloning the template to its first published, discoverable, monitored product, against a one-day target. A working golden path lands around six hours. A path with one manual approval step lands at about 30. One with a provisioning ticket lands at 74. One requiring a platform engineer lands at 120. The number that matters is elapsed time rather than adoption, because adoption can be high simply because the path is mandatory.working golden path6 h+ one approval step30 h+ provisioning ticket74 h+ platform engineer120 hthe path is brokenone day

The measure of the golden path is not adoption but time. A new domain reaching a published, monitored, discoverable product in under a day means the path works; three days means there is a manual step somewhere the platform team has stopped seeing because they know the workaround.

SLA Targets & Performance Baselines

Metric Target Alert threshold Remediation
Time to first published product < 1 day for a new domain > 3 days Instrument the path; find the manual step
Reconcile idempotency 0 changes on unchanged manifest Any non-zero A step’s fingerprint is non-deterministic
Reconcile duration < 10 min full provisioning > 30 min Parallelise independent steps
Platform capability availability 99.9% < 99.5% rolling 1h Platform on-call
Escape-hatch usage Tracked, not minimised Rising sharply on one capability That capability needs work
Manual platform tickets < 2 per domain per quarter > 5 A capability is missing or undiscoverable

The escape-hatch row is deliberately not a target to minimise. A platform whose escape hatches are never used is either perfect or mandatory, and it is rarely the first. Rising substitution of one capability is the clearest signal available that the capability is worse than what a domain can build themselves, and suppressing it removes the signal without fixing the cause.

A note on documentation, because it is where most platform effort is wasted. A platform whose capabilities are documented but not discoverable from the tools a domain already uses will be under-used regardless of how good the documentation is. The discoverability that actually works is in-band: the reconciler naming the capability that would have handled a step a domain wrote by hand, the manifest schema carrying descriptions that surface in an editor, the error message from a failed publish naming the gate and linking to what it checks. Every one of those reaches a domain engineer at the moment they need it, which no wiki page does.

The corollary is that platform errors are a user interface. An error saying “reconcile failed” teaches nothing and produces a support ticket; one saying “step transformation diverged: manifest declares NTv2:national-grid-2024, provisioned pipeline is Helmert:7param-1998 — re-apply to converge” resolves itself. Investing in the error text of the ten most common failures typically removes more platform toil than any new capability.

None of this survives without the platform team using their own path regularly. A team that provisions its reference products by hand, because they know the shortcuts, will not notice the golden path decaying until a domain reports it — by which point the domain has already built a workaround they will keep using.

Governance & Compliance Notes

The platform team’s accountability is for capability, not for correctness of domain data — and confusing the two is how platform teams become approval bodies. If a domain publishes a product with poor accuracy, that is the domain’s to answer for; if the platform’s quality gate failed to run, that is the platform’s. Keeping the line clear is what allows the platform to be operated as a product with its own SLOs rather than as a governance function.

Compliance obligations do attach to the platform where it holds the enforcement point. Because access control and cost attribution have no escape hatch, the platform is the authoritative record of who accessed which product over which extent, and that record inherits the retention requirements of the most regulated domain it serves. Where a domain is subject to jurisdictional residency constraints, the platform’s provisioning must be able to place that domain’s store and compute in a compliant region from the manifest alone — a capability whose absence forces the domain out of the platform entirely, which is the worst available outcome for both compliance and visibility.

Finally, the manifest schema is itself a governed artifact. Adding a required field to it is a breaking change for every domain, and it deserves the same versioning discipline the platform asks of spatial products: a version bump, a migration window, and a period where both shapes are accepted. A platform that changes its own interface without that discipline while requiring it of others will not be trusted with the parts of the estate that matter.