Writing a Spatial Data Product Charter

A charter is the one-page answer to “what is this product, who is it for, and what does it promise” — written before the first pipeline runs, because every later decision refers back to it. Without one, a spatial dataset accumulates consumers it was never designed for, an SLO chosen by whoever set up the alert, and a resolution nobody can justify. This guide writes a charter that is short enough to be read and specific enough to settle arguments, and shows how each clause turns into something enforced. It gives the product model in Product Thinking for GIS Datasets a concrete artifact, within Geospatial Data Mesh Fundamentals.

Prerequisites

Requirement Value / Assumption Notes
Tools A text editor and the registry API The charter is versioned with the product
Inputs At least one named consumer with a stated need A product with no named consumer is a dataset
Measurements Baseline availability, latency and freshness Set SLOs from observation, not aspiration
Access roles domain-owner authors; consumers review A charter written without consumers will be wrong
Environment CATALOG_API, PRODUCT Exported before running

Step-by-Step Implementation

1. Write the charter itself

One page, seven clauses. Anything longer stops being read, which makes it stop being true.

yaml
# charter.yaml — the whole thing. Versioned alongside the contract.
product: cadastral/parcels
owner: team-cadastral
status: Production

purpose: >
  The authoritative geometry and ownership reference for land parcels within the
  national extent, for services that must know where a parcel boundary is and who
  holds it. It is NOT a planning-constraint layer and NOT a valuation source.

consumers:
  - name: logistics/router
    need: "parcel boundaries to 2 m, refreshed within a week"
    access: tiles
  - name: planning/reporting
    need: "ownership attributes, point-in-time reproducible"
    access: snapshot
  - name: public/portal
    need: "boundaries for display only"
    access: tiles

not_for:
  - "Sub-metre engineering survey — use the survey-grade product"
  - "Historic ownership research beyond the 7-year retention"

promises:
  crs: "EPSG:4326"
  resolution: "positional accuracy RMSE < 2.0 m against control:national-geodetic-2024"
  cadence: P90D
  availability: 0.999
  latency_p95_ms: 300
  retention: P7Y

boundaries:
  extent: "national"
  authoritative_for: ["parcel geometry", "ownership reference"]
  defers_to:
    - product: "hydrology/flood_extents"
      for: "flood risk"

review: P180D

Seven clauses, and what each one settlesSeven charter clauses read bottom to top with the argument each one settles. Status governs how tightly the SLO is enforced. Review cadence keeps the charter true. Boundaries settle which product is authoritative for a question. Promises become enforced SLOs and published measurements. Exclusions prevent the misuse that generates most support load. Consumers make the product a product rather than a dataset. Purpose answers, in one sentence, whether a consumer should be using it at all.statuslifecycle statehow tightly the SLO bindsreviewevery 180 dayskeeps it trueboundariesauthoritative for · defers towho owns the questionpromisesCRS · accuracy · cadence · SLObecome enforced targetsnot_fornamed exclusionsprevents most misuseconsumersnamed, with needsa dataset without thempurposeone sentenceshould I use this at all

Verify every promise is measurable — a clause nobody can check is a sentiment:

bash
python3 - <<'PY'
import yaml
c = yaml.safe_load(open("charter.yaml"))
measurable = {"crs", "resolution", "cadence", "availability", "latency_p95_ms", "retention"}
missing = measurable - set(c["promises"])
print("unmeasurable or missing promises:", missing or "none")
PY

2. Turn each promise into something enforced

A charter that is not wired to enforcement decays into documentation within two releases.

Charter clause Enforced by Where
crs Blocking quality gate Publish path
resolution Accuracy measurement against the control set Publish path, advisory
cadence Freshness monitoring Prometheus alert
availability SLO burn-rate rules Prometheus alert
latency_p95_ms Latency SLI Prometheus alert
retention Storage lifecycle policy Object store rules
not_for Nothing automated Review conversation

Which promises are enforced, and which rely on a conversationSix promise clauses against what enforces each and where. CRS is enforced by the blocking quality gate on the publish path. Resolution is measured against a control set and published rather than gated. Cadence, availability and latency all become Prometheus alerts. Retention becomes an object-store lifecycle rule. Exclusions have no automated enforcement at all and rely on the review conversation, which is precisely why they belong in a document a human reads.Enforced byWherecrsblocking quality gatepublish pathresolutioncontrol-set measurementpublish, advisorycadencefreshness alertPrometheusavailability · latencyburn-rate rulesPrometheusretentionlifecycle policyobject storenot_fornothing automatedthe review

bash
# Generate the SLO registration from the charter rather than hand-writing it, so
# the two cannot disagree.
python3 - <<'PY' | curl -sS -X PUT "$CATALOG_API/products/$PRODUCT/slo" \
                        -H 'content-type: application/json' -d @-
import json, yaml
c = yaml.safe_load(open("charter.yaml"))["promises"]
print(json.dumps({
    "availability": c["availability"],
    "latency_p95_ms": c["latency_p95_ms"],
    "freshness": c["cadence"],
}))
PY

3. Review it against reality on a schedule

bash
# The review compares each promise against its measured value over the period.
curl -sS "$PROM/api/v1/query" --data-urlencode \
  'query=avg_over_time(product:availability:ratio{product="'"$PRODUCT"'"}[180d])' \
  | jq -r '"availability measured: \(.data.result[0].value[1])"'
curl -sS "$CATALOG_API/products/$PRODUCT" \
  | jq -r '"accuracy measured: \(.quality.positional_accuracy.rmse_m) m"'

Verify the promises are neither routinely breached nor never binding — both mean the number is wrong:

Reading the review: an SLO that constrains nothing was set too looselyError budget consumed over 180 days for four products, as a percentage, against the 40 percent that indicates a target set at roughly the right level. A product consuming 2 percent has an SLO that has never constrained anything and was set below what it already delivered. One at 38 percent is well calibrated. One at 71 is tight but workable. One at 104 has been breaching, which means the target is beyond what the architecture can deliver. Both extremes are findings; only the middle needs no action.product A2%constrains nothingproduct B38%product C71%tight but workableproduct D104%beyond the architecturewell calibrated

bash
# A promise met 100% of the time for 180 days was probably set too loosely.
curl -sS "$PROM/api/v1/query" --data-urlencode \
  'query=1 - avg_over_time(product:error_budget_consumed{product="'"$PRODUCT"'"}[180d])' \
  | jq -r '"error budget unused: \(.data.result[0].value[1])"'

4. Publish it where consumers look

bash
curl -sS -X PUT "$CATALOG_API/products/$PRODUCT/charter" \
  -H 'content-type: application/yaml' --data-binary @charter.yaml
curl -sS "$CATALOG_API/products/$PRODUCT" | jq '.charter.purpose, .charter.not_for'

Configuration Reference

Clause Required Purpose
purpose Yes What this is, in one sentence a consumer can act on
consumers Yes, ≥ 1 named A product with no named consumer is a dataset
not_for Yes Prevents the misuse that generates most support load
promises Yes, all measurable Each maps to an enforcement point
boundaries.authoritative_for Yes What this product is the source of truth for
boundaries.defers_to Where applicable Prevents two products claiming one question
review Yes A charter never reviewed is a charter never true
status Yes Lifecycle state, which governs SLO enforcement

Common Failure Modes & Fixes

The charter is written and never read again. Root cause: nothing refers to it. Fix: generate the SLO registration from it, surface purpose and not_for in the catalog entry, and review on the stated cadence.

Every promise is met perfectly for a year. Root cause: the promises were set below what the product already delivered, so they constrain nothing. Fix: set SLOs from measured baselines with genuine headroom, not from whatever the product happens to do.

Consumers use the product for something in not_for. Root cause: the alternative named there does not exist or is worse. Fix: not_for only works when it points somewhere; an exclusion with no destination is ignored.

The consumer list is stale. Root cause: it was written once from memory. Fix: reconcile it against access telemetry at each review; a caller absent from the charter is either a consumer nobody planned for or a dependency nobody knows about.

Two products both claim to be authoritative for the same thing. Root cause: neither charter has a defers_to. Fix: the review is where this surfaces; the resolution is an ownership decision, and writing it down is what stops it recurring.

FAQ

Is a charter not just the contract with extra words?

They serve different readers and different moments. The contract is machine-readable and answers “what will this return, in what shape” — it is consumed by validators and consumer tests. The charter is human-readable and answers “should I be using this at all”, which no schema can express: purpose, exclusions, what the product is authoritative for and what it defers to. A consumer reads the charter once when choosing, and their code reads the contract on every request. Neither substitutes for the other, and the charter is the one that prevents the misuse that contracts cannot detect.

How long should it be?

One page, and the constraint is the point. A three-page charter is a design document, and design documents are read at authoring time and never again — at which point the clauses that would have settled a later argument are buried where nobody looks. Seven clauses covering purpose, consumers, exclusions, promises, boundaries, review cadence and status is enough to settle most disputes about a product, and short enough that reviewing it every six months is a twenty-minute conversation rather than a project.

Who signs off on the charter?

The owning domain authors it and the named consumers review it, which is a genuine review rather than a formality — a consumer whose stated need does not appear in the promises has learned something important before they build on it. A central governance body should not sign off, because a charter is a statement of what one domain commits to, and requiring central approval reintroduces the queue federation exists to remove. Where two charters conflict — both claiming authority for the same question — that is the case governance should arbitrate.

What happens to the charter when the product is deprecated?

It stays published and its status changes, because a consumer discovering the product during its sunset needs to know both what it was for and that it is going away. The charter is also where the successor is named, which makes it the natural place a consumer looks after seeing a deprecation header. Deleting it at retirement removes the record of what the product was authoritative for, which is exactly what someone reproducing an old analysis needs.

How does the charter relate to the product’s SLO?

The charter states the promises and the SLO enforces a subset of them. Availability, latency and freshness map directly onto monitored objectives; accuracy maps onto a published measurement rather than a paging alert; purpose and exclusions map onto nothing automated at all. Generating the SLO registration from the charter, rather than maintaining both, is what stops the two drifting — and the clauses with no enforcement point are exactly the ones worth reviewing in conversation, because nothing else will surface them.

What if consumers disagree with the promises?

That disagreement is the most valuable output of the review, and it is far cheaper before the product is built than after. A consumer whose stated need does not appear in the promises has learned that this product is not for them, which saves both sides a migration later. Where the need is reasonable and the producer cannot meet it, the honest outcomes are a second product, a different owner, or an explicit entry in not_for — all of which are better than a promise made to end the conversation.