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.
# 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
Verify every promise is measurable — a clause nobody can check is a sentiment:
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 |
# 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
# 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:
# 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
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.
Related
- Product Thinking for GIS Datasets — the parent topic and the output-port model
- Defining SLAs for Spatial Data Products — turning the promises clause into enforced targets
- Data Contracts for Spatial Products — the machine-readable counterpart