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 |
Context
Section titled “Context”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.devis already registered and managed in Cloudflare by the repo owner, and the$idalready names it.MANIFEST.mdwarns to register the domain before first publish and not to encode a namespace such asmbeacom.github.iointo the$id, because ADR-0006 publishes under a personal namespace that may later transfer to an org — a namespace-encoded$idbreaks 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.
Decision
Section titled “Decision”Serve the canonical schema at its $id from the apex domain adrkit.dev, hosted
on GitHub Pages alongside the documentation site.
- The apex
adrkit.devis 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 bybun run schema:emitand guarded in CI byschema-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$idmeans 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
$idcarries 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. ASCHEMA_VERSIONbump 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 onlyv0.1.0exists, 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
$idorigin and path cannot.
Options considered
Section titled “Options considered”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.
Trade-offs
Section titled “Trade-offs”- 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/AAAArecords (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 insite/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.
Consequences
Section titled “Consequences”- Easier: a single canonical origin; validation against a stable published URL
(via
adr lintand 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
$idURL 404s, serves stale or non-canonical bytes, or an org transfer/host change forces the$idto 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.
Action items
Section titled “Action items”- Add
adrkit.devtosite/public/CNAMEso the apex persists across deploys. - Build step deriving the served schema path from the
$idand writing the canonical bytes into the site’s published output. - Byte-match guard asserting the served schema equals
schema/adr.schema.json. - 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. - GitHub Actions workflow building with Bun and deploying to GitHub Pages.
- Prerequisite of the first
SCHEMA_VERSIONbump: make the publish step retain all previously published version files at their exact paths (the current build writes only the current version). Onlyv0.1.0exists 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. - 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).