Building a Golden Path Template for New Domains

A golden path that lives in documentation decays; one that lives in a template repository cannot decay without failing visibly. This guide builds that template: a repository a new spatial domain clones, fills in three values, and runs — producing a provisioned store, a validated product, a registered catalog entry, and a working tile port inside a day. It makes concrete the capability model in Self-Serve Platform Capabilities for Spatial Teams within Geospatial Data Mesh Fundamentals, and the infrastructure it stands up is covered in Provisioning Spatial Infrastructure with Terraform.

Prerequisites

Requirement Value / Assumption Notes
Tools git ≥ 2.40, python3 ≥ 3.11, make, the platform CLI No domain-specific tooling
CRS convention Template defaults to EPSG:4326 storage, WebMercatorQuad tiles Deviating requires a stated reason
Access roles domain-owner for the new domain; no platform-team involvement If a platform engineer is needed, the path is broken
CI A pipeline that runs the template’s own checks on every commit The template validates itself
Environment DOMAIN, PRODUCT, OWNER_TEAM The only three values a new domain supplies

The three-variable constraint is the design goal, not a simplification for this guide. Every additional value a new domain must decide is a decision they are not yet equipped to make, and each one lengthens time-to-first-product measurably.

Step-by-Step Implementation

1. Structure the template around one manifest and one command

The repository’s shape is itself the teaching. A domain engineer opening it should see immediately where their data is described and how it gets published, with no directory whose purpose needs explaining.

bash
# The whole template. Nothing here is optional and nothing is a placeholder to fill in
# beyond the three values in product.yaml.
spatial-domain-template/
├── product.yaml            # the entire declaration — CRS, ports, quality, SLO
├── Makefile                # make plan | make apply | make publish | make verify
├── pipeline/
│   ├── ingest.py           # source → canonical EPSG:4326, with the CRS declared
│   └── quality.sql         # the blocking gate, unmodified from the platform default
├── tests/
│   └── test_contract.py    # consumer-driven contract test, runs in CI
└── .github/workflows/
    └── publish.yml         # plan on PR, apply and publish on merge

Verify the template stands up unmodified before a domain ever touches it:

bash
git clone https://git.internal/platform/spatial-domain-template demo-domain
cd demo-domain && DOMAIN=demo PRODUCT=sample OWNER_TEAM=platform make plan
# A template that cannot plan from a clean clone is already broken.

2. Make the defaults opinionated and the deviations explicit

Every value in the manifest that a domain could reasonably leave alone should already be filled in with the platform’s opinion, and every deviation should require a comment explaining it.

yaml
# product.yaml — the three variables are at the top; everything below is the opinion.
apiVersion: mesh.geospatial/v1
kind: SpatialProduct
metadata:
  domain: ${DOMAIN}              # ─┐
  name: ${PRODUCT}               #  ├─ the only three values a new domain supplies
  owner: ${OWNER_TEAM}           # ─┘
spec:
  storage:
    class: postgis
    crs: "EPSG:4326"             # canonical; deviate only for survey-grade metric work
    retention: P7Y
  source:
    crs: "REPLACE_ME"            # must be declared — the pipeline fails closed without it
    transformation: "REPLACE_ME" # pinned; never auto-selected
  quality:
    blocking: [geometry_validity_rate, crs_conformance, coordinate_precision]
    precision_bound: 6
  ports:
    - type: tiles
      matrixSet: WebMercatorQuad
      zoom: { min: 6, max: 14 }  # extend upward only where demand is measured
  slo:
    availability: 0.999
    latency_p95_ms: 300
    freshness: P1D
  access:
    principals: []               # default-deny; add principals deliberately
    spatialScope: extent

Which values a domain supplies, and which the template has already decidedSeven manifest values against who supplies them and what a deviation requires. Domain, product name and owner are the only three a new domain provides. Source CRS and transformation must be declared and fail closed rather than defaulting, because guessing produces valid geometry in the wrong place. Storage CRS, tile matrix set, quality gates and SLO all carry the platform opinion, and deviating from each requires a stated reason rather than a preference.Supplied byDeviation requiresdomain · name · ownerthe new domainsource.crsthe new domainfails closed if absentsource.transformationthe new domainfails closed if absentstorage.crstemplate: EPSG:4326survey-grade metric workmatrixSettemplate: WebMercatorQuadconsumers on another gridquality.blockingtemplate: the floornever

Verify that the two REPLACE_ME values genuinely block rather than defaulting:

bash
make plan 2>&1 | grep -q 'source.crs must be declared' \
  && echo "fails closed on an undeclared source CRS"

3. Make the Makefile the only interface

A domain should never need to know which tool runs underneath. Four targets cover the entire lifecycle, and each one is safe to run repeatedly.

makefile
# Makefile — the platform's whole surface for a domain team.
.PHONY: plan apply publish verify

plan:            ## Show what provisioning would change. Read-only, always safe.
	mesh-platform apply --manifest product.yaml --dry-run

apply:           ## Reconcile infrastructure toward the manifest. Idempotent.
	mesh-platform apply --manifest product.yaml

publish:         ## Run the pipeline and publish a partition. Gated on quality.
	python3 pipeline/ingest.py --manifest product.yaml
	psql -f pipeline/quality.sql -v product=$(PRODUCT) -v partition=$(PARTITION)
	mesh-platform publish --manifest product.yaml --partition $(PARTITION)

verify:          ## Prove the product is discoverable, routable and within SLO.
	mesh-platform verify --manifest product.yaml --check catalog,routing,slo

Four commands covering the whole lifecycle, each safe to run repeatedlyThe template exposes four targets and nothing else. Plan is read-only and shows what provisioning would change. Apply reconciles infrastructure toward the manifest and is idempotent. Publish runs the pipeline and publishes a partition, gated on the quality check. Verify proves the product is discoverable, routable and within its SLO. Two rails record where a run stops: an undeclared source CRS fails at plan, and a failed quality gate stops the publish with the previous version left live.make planread-onlymake applyidempotentmake publishgatedmake verifycatalog · routing · SLOprovisionmaterializeprovesource.crs undeclaredquality gate blockedStops with a named reasonprevious version stays live

Verify idempotency, which is what makes the path safe for someone learning it:

bash
make apply && make apply | tee /tmp/second.log
grep -q '0 changes' /tmp/second.log && echo "second apply is a no-op"

4. Have the template test itself in CI

A golden path that is not exercised on every change is a golden path that has already diverged from reality.

What CI runs on every platform commit, so the path cannot decay unnoticedFive stages the template runs against an ephemeral environment on every commit to the platform. It plans from a clean clone, which catches a template that cannot start. It applies, then applies again and asserts zero changes, which catches non-deterministic provisioning. It publishes a clean fixture partition and then a deliberately defective one, asserting that the quality gate blocks the second. Finally it verifies discoverability and routing. A platform change that breaks the golden path breaks this build rather than a domain.plan, clean clonecan it startapplyprovisionsapply againasserts 0 changespublish fixturesclean, then defectiveverifycatalog · routingA platform change that breaks the path breaks this build, not a domain

yaml
# .github/workflows/publish.yml — the template proves itself, continuously.
name: publish
on: [pull_request, push]
jobs:
  golden-path:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Plan from a clean clone
        run: DOMAIN=ci PRODUCT=smoke OWNER_TEAM=platform make plan
      - name: Apply against the ephemeral environment
        run: DOMAIN=ci PRODUCT=smoke OWNER_TEAM=platform make apply
      - name: Confirm the second apply is a no-op
        run: |
          DOMAIN=ci PRODUCT=smoke OWNER_TEAM=platform make apply | tee out.log
          grep -q '0 changes' out.log
      - name: Publish a fixture partition and verify
        run: |
          DOMAIN=ci PRODUCT=smoke OWNER_TEAM=platform PARTITION=fixture make publish
          DOMAIN=ci PRODUCT=smoke OWNER_TEAM=platform make verify

Verify the whole path end to end, timing it — the number that matters is elapsed time, not success:

bash
time (make apply && PARTITION=fixture make publish && make verify)
# Under 20 minutes for a fresh domain is a working path; over an hour means a manual step.

Configuration Reference

Field Supplied by Default Deviation requires
metadata.domain The new domain none
metadata.name The new domain none
metadata.owner The new domain none
source.crs The new domain none — fails closed
source.transformation The new domain none — fails closed
storage.crs Template EPSG:4326 A survey-grade metric use case
ports[].matrixSet Template WebMercatorQuad Consumers already on another grid
quality.blocking Template Validity, CRS, precision Never — this is the floor
slo.availability Template 0.999 Measured consumer need
access.principals Template [] — default deny Each addition is deliberate

Common Failure Modes & Fixes

A new domain’s first make apply fails on credentials. Root cause: the template assumes a role the new domain does not yet hold. Fix: role assignment belongs in domain registration, before the template is cloned — if it happens during the golden path, the path starts with a ticket.

Time-to-first-product is a week despite the template working. Root cause: a manual step outside the repository — a firewall rule, a DNS entry, an approval. Fix: instrument timestamps at clone, first apply, first publish; the gap that dominates names the step, and it usually surprises the platform team.

Domains copy the template and then diverge immediately. Root cause: the defaults are wrong for a common case, so everyone edits the same three lines. Fix: that edit pattern is the signal — change the default rather than documenting the workaround.

CI passes but a real domain’s first publish fails on quality. Root cause: the fixture partition is synthetic and clean, so the gate is never exercised against realistic data. Fix: include a deliberately defective fixture alongside the clean one and assert the gate blocks it.

The template drifts from what the platform actually supports. Root cause: nobody runs it except new domains, which is rare. Fix: the CI job above runs it on every commit to the platform, so a platform change that breaks the path breaks a build rather than a domain.

FAQ

How opinionated should the defaults be?

Uncomfortably so. Every option offered is a decision a new domain has to research, and a template with a dozen choices at each step is a research project rather than a route. The right test is whether a domain engineer who knows their data but nothing about the platform can get to a published product without reading anything beyond the manifest’s comments. That standard forces defaults for storage CRS, tile matrix set, zoom range, quality gates and SLO — and it is met by making deviation possible and slightly effortful, so a domain with a real reason takes the escape hatch and a domain without one does not think about it.

Should the template include a sample dataset?

Include a fixture, not a sample. A fixture exists so CI can exercise the whole path end to end, and it should be small, synthetic, and paired with a deliberately defective twin so the quality gate is proven to block. A realistic sample dataset is a different thing and tends to become a maintenance burden — it ages, it acquires domain-specific assumptions, and new domains start editing around it rather than replacing it. The fixture should be obviously not-real, so nobody mistakes it for a starting point.

What happens when the platform’s manifest schema changes?

The same discipline the platform asks of spatial products applies to the platform’s own interface: a version bump, a window during which both shapes are accepted, and a migration note. The template is updated in the same change, and because CI runs the template on every platform commit, a schema change that breaks it fails immediately rather than reaching a domain. Domains already provisioned continue on the old schema until they update, which is what makes the change non-breaking in practice rather than only in intent.

How do we know the golden path is still the fastest route?

Measure elapsed time, not adoption. Adoption can be high because the path is mandatory; time-to-first-published-product cannot be faked. Record timestamps at clone, first successful apply, and first successful publish, and watch the distribution across new domains. A path that took a day six months ago and takes three now has accumulated a manual step that the platform team has stopped seeing because they know the workaround.