Host your own corpus
The tutorial walks the spoke path as a sequence of steps. This page is the reference for the model underneath it: what a spoke is, how it is laid out, and how it coexists with a central corpus.
The federated model
A repo does not have to only consume a central corpus. It can document itself - host its own corpus, built and served in place, specific to that repo and never merged upstream. That is a spoke, and it is the federation seam the topology was designed around.
The defining fact: a spoke's source is self: true. The source being documented is the enclosing repo, and its drift referent is the repo's own git HEAD. Documentation and code therefore share one commit history, and provenance comes nearly free - the property the federated model always promised.
The .alloy/ layout
alloy init --spoke scaffolds a portable root:
.alloy/
├── alloy.config.yaml the source registry - a single self: true source
├── ecosystems.yaml what counts as a source file per ecosystem
├── rulepacks/
│ └── templates/ rule packs + doc templates (survey-drafted)
└── corpus/
├── meta/corpus-manifest.yaml the spoke's own consumer contract
├── shared/ curated docs
└── sources/<name>/ generated docsEverything is committed to the repo. There is no separate state store: the committed manifest is the corpus's declaration, the same "committed = declared" principle the central lock rests on.
The self: true source
A spoke source is deliberately minimal:
corpusRoot: .alloy/corpus
sources:
your-repo:
self: true # this repo is the source; referent = local HEAD
scanners: [js-workspace]
units:
- kind: lib
match: packages/{name}
groupBy: name
# coverage: { ... } # opt into accounting once you are triagingIt carries no repo/ref (those are optional-informational only in a spoke) and no path/track/drift - the referent is always local HEAD. Keep machine-local paths out of it entirely; it is committed and pathless.
Running the loop in a spoke
Every command runs from anywhere inside the spoke repo; the CLI walks up to .alloy/alloy.config.yaml and resolves the corpus home (.alloy/corpus/) from there. Because a spoke may not depend on the package, the CLI can be invoked directly:
npx @engineering11/alloy-cli discover
npx @engineering11/alloy-cli statusUnlike a central source that reads an external checkout, a self: source needs no ALLOY_SOURCE_PATH_* override, and its tree is legitimately dirty with the authored docs at stamp time - self mode warns rather than refuses.
Coexisting with a central pin
A repo can carry both a central pin and its own spoke corpus, and they resolve without ambiguity because the split is by category vocabulary, not by transport:
- The central
.alloy/corpus.lockstays hub-only -{ version, view }, a strict schema that takes no spoke entry. - The spoke needs no lock entry at all; its committed manifest is its declaration.
The retriever gains a rung 0: a query key whose category the spoke manifest declares resolves against the spoke corpus and is terminal - an in-vocabulary miss does not fall through. A key whose category the spoke does not declare skips rung 0 and runs the central ladder governed by the lock's { version, view } exactly as before.
The consequence is the shadowing rule: a spoke category eclipses a same-named central category for that repo. So pick non-colliding category names unless the eclipse is intended - and spoke-mode survey --check warns you on a collision, which is the moment to make that call deliberately.
Self-updating CI
alloy init --spoke --ci (or --ci on an existing spoke) scaffolds three workflows that keep the corpus honest without ever auto-merging:
alloy-detect.yml- daily drift detection to a single pinned issue.alloy-propose.yml- a weekly propose run that opens one human-merged PR.alloy-validate.yml- corpus validation on every PR touching.alloy/**.
Detection and proposal automate; the merge is always a human. That is the same automation ladder the central corpus runs, scaled to a spoke.