Skip to content

ADR-0011: Host the canonical JSON Schema at its $id on adrkit.dev

Field Value
Status accepted
Date 2026-07-18
Schema version 0.1.0
Reversibility one-way-door
Blast radius org
Scope org
Tags schema, hosting, docs, packaging
Deciders @mbeacom
Authored by agent-drafted
Ratified by @mbeacom
Review tier arb
Review reason The $id origin and path are a one-way door for the life of the major version; every editor, validator, and $ref that pins the schema breaks if it moves.
Relates to 0002, 0006, 0010
Affects path:schema/adr.schema.json, path:site/**, path:.github/workflows/site.yml
Source docs/adr/0011-host-the-canonical-json-schema-at-its-id-on-adrkit-dev.md

The decision-record schema declares an absolute $id: https://adrkit.dev/schema/adr/v0.1.0/adr.schema.json (derived from SCHEMA_VERSION in packages/core/src/schema/emit.ts). A JSON Schema $id is not decorative: editors resolve it to fetch the document behind a # yaml-language-server: $schema= modeline, and JSON-Schema $ref resolution dereferences it. If nothing serves the exact bytes at that URL, $schema references and cross-schema $refs do not resolve, and the editor validation that makes typed frontmatter worthwhile silently does not happen.

So the URL in the $id is a promise. Something must keep it.

Two forces make this decidable now rather than later:

  • We are standing up a documentation site anyway. A static docs deploy can serve a static JSON file at a fixed path for free, so the schema host and the docs host can be the same origin with no extra infrastructure.
  • adrkit.dev is already registered and managed in Cloudflare by the repo owner, and the $id already names it. MANIFEST.md warns to register the domain before first publish and not to encode a namespace such as mbeacom.github.io into the $id, because ADR-0006 publishes under a personal namespace that may later transfer to an org — a namespace-encoded $id breaks every pinned reference on transfer.

The load-bearing property is that the host and path are a one-way door for the life of the major version. Once editors, CI in other repositories, and third parties pin https://adrkit.dev/schema/adr/v0.1.0/adr.schema.json, moving it breaks all of them. ADR-0002 already ties schema evolution to semver; this record extends that discipline to the location the schema resolves from.

Serve the canonical schema at its $id from the apex domain adrkit.dev, hosted on GitHub Pages alongside the documentation site.

  • The apex adrkit.dev is the fixed origin for the major version. It is not a namespace-encoded host, so an eventual org transfer (ADR-0006) does not force a change.
  • The served copy is derived from the source of truth, never hand-copied. A build step reads schema/adr.schema.json (regenerated by bun run schema:emit and guarded in CI by schema-emit-matches), reads the served path from the schema’s own $id, and writes the bytes verbatim into the site’s published output. Deriving the path from $id means a version bump moves the file automatically and cannot silently mismatch.
  • A guard asserts the served bytes are byte-identical to the canonical file. If they can drift, that is a bug, not a nuance. Version-bearing URLs in the docs are derived from the canonical $id/SCHEMA_VERSION, and a build-time check fails if any operational doc references a stale schema version.
  • Every published version path is immutable. Because the $id carries the full version (/schema/adr/v<version>/adr.schema.json), each release is a distinct URL. Once a version is published, that exact path must keep serving that version’s bytes for the life of the major version. A SCHEMA_VERSION bump introduces a new path and must retain every previously published version file — a bump may add URLs but may never remove or repoint one. Today only v0.1.0 exists, so the current single-version build satisfies this trivially; retaining prior files becomes a hard prerequisite of the first version bump (see action items).
  • The docs framework is a lighter, reversible choice. Astro with the Starlight theme produces static HTML, which satisfies ADR-0010’s Node-targeted/portable-artifact boundary. The framework can be replaced later without touching this record; the $id origin and path cannot.

Option A: Serve at adrkit.dev via GitHub Pages, path derived from $id (chosen)

Section titled “Option A: Serve at adrkit.dev via GitHub Pages, path derived from $id (chosen)”
Dimension Assessment
Matches existing $id Exact — no schema change needed
Infrastructure None beyond the docs deploy already wanted
Transfer-safety Apex domain, not namespace-encoded
Drift risk Eliminated by deriving path from $id + byte-match guard
Cost DNS records + TLS provisioning, one-time

Option B: Serve from a namespace URL (mbeacom.github.io or raw.githubusercontent.com)

Section titled “Option B: Serve from a namespace URL (mbeacom.github.io or raw.githubusercontent.com)”

Pros: zero DNS work; GitHub provisions TLS automatically. Cons: disqualified twice over. The $id already says adrkit.dev, so this would require either changing the $id (a breaking schema change on a v0 that has not shipped, and a recurring temptation every transfer) or serving from a URL that does not match the $id (which does not resolve $refs). It also encodes a personal namespace that ADR-0006 and MANIFEST.md explicitly forbid in the $id.

Option C: Do not host; ship the schema only inside the package

Section titled “Option C: Do not host; ship the schema only inside the package”

Pros: nothing to deploy or keep alive. Cons: an absolute $id that resolves to nothing is worse than no $id. Editors following the modeline get a fetch failure, $ref resolution across documents fails, and the schema’s whole reason for being URL-addressable — that any tool in any language can pull it — is forfeited.

Option D: Serve from a dedicated subdomain (schema.adrkit.dev)

Section titled “Option D: Serve from a dedicated subdomain (schema.adrkit.dev)”

Pros: separates schema availability from docs; could scale independently. Cons: the $id targets the apex, not a subdomain, so this has Option B’s mismatch problem. The separation is also premature — a static file on the same Pages origin has no meaningful scaling ceiling for this traffic. Rejected for now; if schema traffic ever warrants it, the move must preserve the apex URL bytes via redirect or proxy, not relocate the $id.

  • The hostname becomes load-bearing infrastructure, not just marketing. A docs-site outage is now also a schema-resolution outage for anyone validating live against the $id. Consumers who need availability guarantees should vendor the schema; the URL is a convenience and a contract of content, not an SLA.
  • Apex on GitHub Pages requires the four GitHub Pages A/AAAA records (or Cloudflare CNAME-flattening) and an initial DNS-only (unproxied) window so GitHub can provision the TLS certificate. That is a one-time operational cost the owner must complete in Cloudflare; it is documented in site/DEPLOYMENT.md.
  • The origin and path are fixed for the major version. A future breaking schema change gets a new v<major> path segment, not a new host.
  • Easier: a single canonical origin; validation against a stable published URL (via adr lint and any JSON-Schema validator); dogfooding — the project’s own schema resolves at its own domain.
  • Harder: DNS and TLS are now part of the release surface; the host cannot be renamed without a coordinated major-version migration.
  • How we would know this was wrong: the $id URL 404s, serves stale or non-canonical bytes, or an org transfer/host change forces the $id to move and breaks pinned references. Any of these is a governance failure, not a routine fix.
  • Revisit if: schema traffic or availability requirements outgrow GitHub Pages (move to a dedicated host, but preserve the exact bytes at the exact apex path via redirect/proxy), or if the domain registration lapses.
  1. Add adrkit.dev to site/public/CNAME so the apex persists across deploys.
  2. Build step deriving the served schema path from the $id and writing the canonical bytes into the site’s published output.
  3. Byte-match guard asserting the served schema equals schema/adr.schema.json.
  4. Derive version-bearing URLs in the docs from the canonical $id, and add a build-time check that no operational doc references a stale schema version.
  5. GitHub Actions workflow building with Bun and deploying to GitHub Pages.
  6. Prerequisite of the first SCHEMA_VERSION bump: make the publish step retain all previously published version files at their exact paths (the current build writes only the current version). Only v0.1.0 exists today, so no retention logic is required yet — but a bump must not ship until this lands, or it will 404 references pinned to the prior version.
  7. Owner: add the GitHub Pages apex DNS records in Cloudflare, initially DNS-only, then enable “Enforce HTTPS” once the certificate issues (see site/DEPLOYMENT.md).