Skip to main content
Private Preview·Early access by invitation.Request access →
Kirimana.
Docs · Governance

Set up hub-and-spoke domain governance

Hub-and-spoke is how a Kirimana project stays governable as it grows. A central hub — the platform team — publishes the standards every domain must meet: classification defaults, approval counts, ownership rules. Each spoke — a domain like sales, hr, or finance — owns its own contracts and moves at its own pace, inside those standards. Nothing is centrally bottlenecked, and nothing is ungoverned.

Git is the single authority. Domains live in kiri.yml; contracts live in files; every catalog is a materialisation of that library, never an independent source of truth. Get the structure right once and the rest of the governance surface — CODEOWNERS routing, PR-time gates, approval counts — falls out of it mechanically.

Domains are the unit of ownership

A domain is a named business area with an owner and a policy. Declare them in kiri.yml:

domains:
  sales:
    owners: [sales-team, alice@acme.com]
    default_classification: internal
  hr:
    owners: [hr-team]
    default_classification: confidential
  finance:
    owners: [finance-team]
    default_classification: internal

Every domain must have at least one owner — that is a hard invariant, not a convention. Ownership is what drives CODEOWNERS auto-routing and the owner-validity lint (see the next two pages). A domain with a placeholder owner (owner@example.com, TODO) is a lint failure, so you cannot ship a domain nobody is accountable for.

The domain name is load-bearing. It is the parent directory of the domain’s config, the prefix of its contract URNs (kirimana:contract:sales.customer:1), and the key CODEOWNERS routes on. Rename a domain and you rename all three — treat the name as stable once contracts reference it.

Per-(domain, layer) contracts

Inside each domain, contracts are organised by medallion layer. The canonical shape is one contract file per (domain, layer) pair01-config/<domain>/bronze.yml, silver.yml, gold.yml, and optionally pit.yml. Each file is a single ODCS v3 DataContract whose schema:[] array lists every table in that layer. A three-layer domain is three files, not eighty.

01-config/
  finance/
    bronze.yml     # raw ingested tables
    silver.yml     # deduped, typed, business-ready
    gold.yml       # roll-ups and marts

Adding a table is a schema:[] append inside the existing file — no new file, no new directory. This keeps the promotion story simple: the catalog binding lives in exactly one place per file (the typed targets: block), so promoting dev → prod is a target switch, not a sweep across dozens of contracts.

Two rules the validator enforces hard:

  • Never split a (domain, layer) across files. One file per pair.
  • Never put per-table catalog/schema bindings in a layer file. Catalog binding has exactly one home — the targets: block.

Run kiri layer validate -p . to check both. A freshly scaffolded project returns zero findings by design.

Layer-wide defaults, per-table overrides

Defaults cascade through a precedence ladder so you set the common case once and override only where a table genuinely differs. When a kiri.* property is resolved for one column on one table, Kirimana looks in order:

  1. Column-levelschema[t].properties[c].customProperties
  2. Table-levelschema[t].customProperties
  3. Layer-wide — the file’s top-level customProperties
  4. Built-in default — closed-menu, fail-closed

So a silver.yml that declares kiri.classification: internal at the top makes every table in that layer internal unless a specific table or column raises it to confidential. You write the exception, not the rule. This is the mechanism the hub uses to make a standard stick: a hub-published default sits at the layer level, and a spoke that needs to deviate does so explicitly and visibly in the diff.

The hub publishes, the spokes own

Put the pieces together and the topology is clear:

  • The hub owns kiri.yml. Domain definitions, default classifications, per-domain approval counts, the auth block, and the RBAC bindings all live here. Editing this file is a platform-admin-only capability — it is the standards document. The hub publishes a standard by setting a layer-wide or domain-level default; spokes inherit it automatically.
  • The spokes own their contracts. A domain team edits the files under 01-config/<their-domain>/, adds tables, and moves data through the medallion layers on its own cadence. They cannot touch another domain’s contracts — CODEOWNERS routing and the PR gate see to that.

Cross-domain edges are the one place spokes must coordinate, and the validator makes the coordination symmetric: if a consumer in one domain declares a kiri.upstream reference to a producer in another, the producer must declare a matching kiri.downstream_consumers entry. Neither side can quietly depend on — or expose to — the other. An unmatched edge is a hard validator failure, so a cross-domain dependency is always a two-sided, reviewed decision.

Where this leaves you

Once the structure is in place, governance is emergent rather than bolted on. The hub changes a default in kiri.yml; every spoke that did not override it picks the change up on its next apply. A spoke adds a table; the owner is already known, the classification default is already set, the CODEOWNERS line already routes the PR. You did not configure any of that per-table — it followed from the domain and layer the table lives in.

The next pages build directly on this: Domain owners + CODEOWNERS turns domain ownership into review routing, RBAC roles + capabilities scopes who may act in which env and domain, and PR-time governance gates enforces the whole thing in CI.

Updated 5 July 2026 · v1.0.0-beta.1