The CLI fetches components at runtime rather than bundling them, so their source is a setting. Four ways to change it, most specific first.
Resolution order
| # | Source | Set by | Use for |
|---|---|---|---|
| 1 | FACET_REGISTRY_DIR | env, a local directory | Testing a registry build without publishing it |
| 2 | --registry <url|path>, or registry in facet.json | flag or config | Forks, private registries, version pins |
| 3 | FACET_REGISTRY_URL | env, a URL | CI, or a machine-wide default |
| 4 | https://facet.astra-void.xyz/r | built in | The published registry |
A value starting with http:// or https:// is read as a URL. Anything else is resolved as a
filesystem path.
FACET_REGISTRY_DIR=site/r npx facet-rbxts listnpx facet-rbxts add button --registry https://ui.example.com/rFACET_REGISTRY_URL=https://ui.example.com/r npx facet-rbxts list{ "registry": "https://ui.example.com/r"}The config field is the one for a team — everyone who clones the project gets the same registry.
https://facet.astra-void.xyz/r is a constant in the published package. That is why the deploy generates a CNAME as part of the site artifact. An Actions artifact replaces the whole site, and without it GitHub can revert the custom domain to the github.io default.
What a registry has to serve
Three kinds of file over plain HTTP (or a directory with the same layout):
<base>/index.json the index — every item, its files, dependencies, and tokens<base>/button.json one payload per item, source text inlined<base>/utils.jsonThe CLI reads index.json first and refuses anything whose version is not 1, telling the user
to upgrade rather than guessing. Item payloads are fetched lazily, only for what is being added,
removed or diffed.
A 404 on an item reads as a typo, a 404 on the index as a moved registry. Requests carry
accept: application/json and time out after 15 seconds.
The full shape is in the registry format reference.
Running a fork
The Facet repo builds its registry with one script, and a fork inherits it:
pnpm registry:check # structural validation — names, types, dependency resolutionpnpm registry:build # registry/ → site/site/ is generated and never committed, so the published registry cannot drift from registry/.
The build emits the index, one JSON per item, and an immutable r/<sha>/ copy of both. It also
emits a landing page, a CNAME, a .nojekyll, schema.json and revisions.json. Without the
.nojekyll, Pages runs Jekyll, which eats underscore-prefixed paths.
schema.json is generated from the FacetConfig type rather than checked in, so it cannot lag the
CLI. It is typed to cover every key, so a new config field does not compile until it is described.
Pages deploys one artifact and it replaces the entire site, so a revision written by one deploy would be deleted by the next. The published tree therefore lives on an accumulating gh-pages branch: the workflow checks it out, copies the new build over it, commits, and uploads that tree.
The branch is storage, not a deployment source: Pages still deploys from Actions, and .git is removed before the upload. A fork that does not need revisions can upload site/ directly.
Test against the working tree before publishing anything:
FACET_REGISTRY_DIR=site/r npx facet-rbxts listFACET_REGISTRY_DIR=site/r npx facet-rbxts add button --cwd ../some-projectDEFAULT_REGISTRY_URL in the CLI’s registry source, $schema in its config module, and CUSTOM_DOMAIN in the build script. A mismatch means the CLI asks a host the site no longer claims.
Adding your own components to a fork
Three edits, and it is not done until all three are made:
- The source in
registry/src/ui(orlib,hooks), addressing other registry files with~/— never a relative path, which the CLI cannot rewrite. - An entry in
registry/registry.tsdeclaring every import as adependency(npm) or aregistryDependency, plus the semantictokensits classes name sofacet doctorcan check them. - A scene in
apps/playground— the only place anyone sees it render.
registry:check enforces the structural half. Unique names, known item types, resolvable registry
dependencies, no file claimed by two items, and one spec per npm package across the whole
registry. facet add unions dependency strings, so two spellings would both reach the package
manager.
What it cannot check is a missing npm dependency — an import the entry never declared, which ships a file that cannot compile.
A private registry
Nothing about the format requires GitHub Pages — any static host works, and so does a directory on
disk. The CLI has no credential support, so for anything behind auth point FACET_REGISTRY_DIR at a
checkout your build already has access to.