Registries
Install component source from the official registry, pin it to a commit, and publish a registry of your own
The remix CLI copies component source into your application and leaves you
owning it. A registry is where that source comes from. This guide covers
using the official one, and publishing your own.
The model
Six words carry the whole system.
| term | what it is |
|---|---|
| Registry | A public GitHub repository holding a directory of component source. |
| Namespace | The name your project gives a registry, like @remix. Commands and dependencies use it to say which registry they mean. |
| Preset | One design language inside a registry — vanilla, fortal, or heroui officially. A project picks exactly one. |
| Catalog | The registry.yaml for one preset. It lists items. |
| Item | One installable unit: its templates, its dependencies, what it exports. |
| Pin | The exact commit your project reads from, recorded in remix.yaml. |
Two rules follow from this and explain most of the CLI's behavior:
- Every read goes through the pin. Never a branch, never a tag — the commit
SHA recorded in your
remix.yaml. Therefrecorded next to it is resolved again only when you runregistry update. Moving to newer source is an explicit act. - A network failure never falls back to bundled content. If the registry is unreachable, the command fails and writes nothing.
Install from the official registry
dart run remix_cli:remix init
dart run remix_cli:remix add buttoninit resolves the registry-stable branch of btwld/remix and stores
its full commit SHA. It checks that your preset exists in that branch and
writes remix.yaml. If the branch is not published, initialization fails and
creates no files.
An optional iconLibrary (remix, rounded, or lucide) lives in that same schema 3
file. Templates carry icon placeholders, resolved at install; to switch an
installed project, change the setting and reinstall every installed item that
uses icons with add --diff, then --overwrite, naming them all in one
command.
A CLI that understands the placeholders has to ship before a registry that
uses them.
Omitting --preset selects vanilla. Pass --preset fortal for Fortal, and
--prefix/--ui-path to control naming and location:
dart run remix_cli:remix init --prefix Acme --preset fortal --ui-path lib/design_systemThe result:
schema: 3
prefix: Acme
preset: fortal
paths:
ui: lib/design_system
defaultRegistry: "@remix"
registries:
"@remix":
repository: "btwld/remix"
path: "registry"
ref: "registry-stable"
revision: "<full commit sha>"Commit remix.yaml and, after installation, remix.ownership.json with the
installed source. The ownership record reserves authored and generated targets;
it does not lock content or hash your edits.
init registers the official registry under the namespace @remix and makes
it defaultRegistry, so bare names like add button install from it. Keep
that name: other registries can depend on official items as @remix/theme,
and those dependencies resolve through your project's @remix.
add resolves the item's full dependency graph, renders every template, checks
your pubspec.yaml, and only then writes. Your edits to already-installed
files are preserved.
dart run remix_cli:remix add button --dry-run # print the plan, write nothing
dart run remix_cli:remix add button --diff # show what would change
dart run remix_cli:remix add button --overwrite # replace just this item--overwrite replaces only the item you name. It never touches a dependency
you have customized.
All install modes, including --diff and --dry-run, check target ownership
before writing or running package tools. Existing files without a recorded
owner require the legacy adoption step.
Neither --overwrite nor --adopt can replace a known different owner.
Move to newer source
Changing a pin and installing source are separate steps, on purpose.
dart run remix_cli:remix registry update @remix
dart run remix_cli:remix add button --diff
dart run remix_cli:remix add button --overwriteregistry update resolves the recorded ref again. For the official registry
that is registry-stable, so it moves the pin to the newest promoted commit.
Pass --ref to choose something else:
dart run remix_cli:remix registry update @remix --ref <sha> # pin that exact commit
dart run remix_cli:remix registry update @remix --ref registry-stable # follow the branch againA pin recorded with a SHA or a tag resolves to the same commit every time, so a
bare registry update leaves it where it is. Pass --ref to move it.
registry update validates the new pin and rewrites remix.yaml. It does not
touch a single installed file. You then review with --diff and adopt with
--overwrite, item by item.
Rolling back a pin is git checkout on remix.yaml. That restores the pin,
not your source — installed files are yours, so review and restore them through
your own version control.
Projects from an earlier prerelease
remix.yaml schema 3 is the only shape this CLI reads. Schemas 1 and 2 shipped
in earlier prereleases and named no registry at all — they read from a snapshot
that once shipped inside the CLI, and that snapshot is gone. There is nothing to
migrate a pin from, so such a project is reinitialized:
rm remix.yaml
dart run remix_cli:remix init --prefix Acme --preset vanillaPass the --prefix, --preset and --ui-path the old file recorded; init
writes configuration and preserves installed source and generated adapters.
For schema 1, the old default preset corresponds to vanilla.
CLI 0.1.0 and 1.0.0-beta.0 also wrote schema 3 without an ownership record.
Keep that configuration. In either case, upgrade the project-local CLI, verify
which registry and preset each installed item came from, then explicitly adopt
it. Plain add button --diff now rejects unknown existing targets, as do
--dry-run and --overwrite.
For a Vanilla Button installation, name both Theme and Button because existing unknown dependencies must also be requested explicitly:
dart run remix_cli:remix add theme button --adopt --diff
dart run remix_cli:remix add theme button --adoptThe first command compares the pinned templates with your source without
writing ownership. Review the differences and verify provenance before running
the second command: --adopt is your declaration of ownership, not an automatic
provenance check. Name every other existing item and dependency you need to
adopt; Fortal Button also requires base_button when present.
Adoption preserves authored edits and records ownership in
remix.ownership.json. It otherwise follows a normal install, including
installing missing source, resolving dependencies, generating missing adapters,
and analysis. It cannot be combined with --overwrite. Commit the ownership
record with remix.yaml and installed source. Later previews and explicit
overwrites work without --adopt; do not downgrade to a CLI that ignores the
ownership record.
If provenance is uncertain or presets were mixed, restore a known matching configuration, ownership record (if one existed), and source from version control, or initialize a clean installation and reapply your edits. Changing the prefix, preset, or UI path conflicts with an existing ownership record; renaming or deleting source files does not release its reservations.
Publish your own registry
A registry is a directory in a public GitHub repository. Nothing else — no
service to run, no account, no publishing step beyond git push.
Layout
registry/
index.yaml # presets -> catalog paths
vanilla/
registry.yaml # the catalog
templates/
button/button.dart.tmplindex.yaml is schema 1 and maps each preset name to its catalog:
schema: 1
presets:
vanilla: vanilla/registry.yamlNamespace and preset are different things
This trips people up, so it is worth being precise:
- A namespace (
@acme) identifies your registry inside a consuming project. Your registry files do not declare it; the consumer types it inregistry add. Pick one and tell consumers to use it, because any catalog that depends on your items names it too. - A preset (
vanilla) identifies a design language. The consuming project selects exactly one, atremix init, and it stays fixed.
remix registry add validates your registry against the preset the project
already selected. So every registry a project uses must offer that
project's preset. Publishing only a acme preset means no project that
initialized against the official registry can add you — remix init always
resolves the official registry, which has no acme.
Until a preset can be introduced by a third-party registry, publish under a
preset name the consuming projects already use — vanilla here. Your
namespace is what keeps your items distinct, not your preset name.
The catalog
registry.yaml is schema 2. Each key under items is an item name, matching
^[a-z][a-z0-9_]*$:
schema: 2
items:
theme:
files:
- source: templates/theme/tokens.dart.tmpl
target: "@ui/theme/tokens.dart"
exports:
- theme/tokens.dart
button:
registryDependencies:
- theme
dependencies:
mix_annotations: ^2.2.0-beta.1
devDependencies:
build_runner: ^2.10.1
mix_generator: ^2.2.0-beta.3
files:
- source: templates/button/button.dart.tmpl
target: "@ui/components/button.dart"
generated:
- "@ui/components/button.g.dart"
exports:
- components/button.dart| field | required | meaning |
|---|---|---|
files | yes | Template source to installed target. |
registryDependencies | no | Other items to install first. |
dependencies | no | Pub constraints the installed source needs at runtime. |
devDependencies | no | Pub constraints needed only to build. |
generated | no | Files the consumer's build_runner will produce. |
exports | no | Paths added to the managed UI barrel. |
Path rules the CLI enforces before writing anything:
sourceis relative and starts withtemplates/.targetstarts with@ui/, which stands in for the consumer's configuredpaths.ui. Storing targets this way is what lets one registry serve every project regardless of where it installs.exportsare relative to that same UI directory.- No traversal, no absolute paths, no URI escapes, in any of them.
Templates
A template is ordinary Dart with these placeholders:
| placeholder | becomes | example |
|---|---|---|
{{typePrefix}} | the project's prefix | AcmeButton |
{{valuePrefix}} | its lower-camel form | acmeButtonStyle |
{{icon:import}} | the chosen icon library's import | import 'package:lucide_flutter/lucide_flutter.dart'; |
{{icon:<name>}} | that library's constant for <name> | LucideIcons.bell |
Icon placeholders resolve through the registry's icons.yaml, published beside
index.yaml: libraries names each library's package, import, and class,
and icons maps every canonical name (the remix library's constant) to its
constant in every library. A template that uses {{icon:<name>}} also carries
{{icon:import}} in its import block, and the CLI adds the chosen library's
package for it. Any other {{...}} token is rejected — an unresolved
placeholder is a registry bug, not something to install.
Two invariants worth knowing before you design items
No two items may own the same target. If two components need the same file, that file is a third item both depend on. The CLI rejects overlapping ownership before it writes, across every registry in the graph.
No item may own @ui/ui.dart. That barrel is managed by the CLI, assembled
from every installed item's exports.
Depending on another registry
Within a schema-2 catalog, a dependency may name another registry:
registryDependencies:
- theme
- "@company/icons"A bare name means the owning registry's own item. A qualified name means a registry the consuming project has configured. An item cannot register a registry or redirect one — the consumer stays in control of every source their code is installed from.
Publish and consume
Commit the registry/ directory and push it to the ref your consumers follow.
A consumer with a vanilla project then registers you and installs:
dart run remix_cli:remix init # selects vanilla
dart run remix_cli:remix registry add @acme \
--repository your-org/your-repo \
--path registry \
--ref stable
dart run remix_cli:remix add @acme/buttonregistry add resolves the ref to a commit and records both. Without --ref
it resolves the repository's default branch once and records its name and
commit. Namespaces match ^@[a-z][a-z0-9_-]*$.
Bare item names resolve through defaultRegistry; @acme/button names yours
explicitly.
Choose the ref consumers follow
The ref a consumer registers decides what a later bare registry update does:
| consumer registered | registry update @acme | to move on |
|---|---|---|
a branch, such as stable | moves to the branch's newest commit | nothing extra |
a tag, such as v1 | resolves the same commit again | --ref v2 |
| a commit SHA | resolves the same commit again | --ref <newer sha> |
The official registry follows a branch, registry-stable, that CI moves only
after a commit passes its checks. Do the same: keep a branch you advance only
after testing a commit, and tell consumers to register that branch. Tags still
work as labels, and for consumers who only want to move when they choose a
version.
Test a change before consumers see it
The CLI reads registries from GitHub only, never from a local path. Push the change to a working branch, then install it in a scratch app initialized with the preset your consumers use:
dart run remix_cli:remix registry add @acme \
--repository your-org/your-repo \
--path registry \
--ref my-change
dart run remix_cli:remix add @acme/button --dry-run
dart run remix_cli:remix add @acme/button--dry-run resolves the whole dependency graph and renders every template
without writing, so catalog and template errors surface there. The real add
then installs the source and its package dependencies; build the app to confirm
it compiles. Push fixes to the same branch, run registry update @acme, and
reinstall with add @acme/button --overwrite. Advance the branch consumers
follow once the scratch app is clean.
What is not supported
Public github.com repositories only. Private repositories, GitHub Enterprise,
arbitrary HTTP endpoints, and local paths are outside the current release.
There is no persistent offline cache.
Troubleshooting
| message | cause |
|---|---|
No registry-stable branch is published for btwld/remix. | The official registry has not been promoted yet, so init has no commit to pin. Nothing was written; retry once it is published. |
GitHub ref <ref> not found | The branch, tag or commit does not exist in that repository. Check the --ref value. |
GitHub repository not found | The --repository is misspelled, or the repository is private. |
Registry <repo> has no <preset> preset. | The registry's index.yaml does not list your project's preset. |
<a> and <b> both target <path>. | Two items claim the same installed file. Extract it into a shared item. |
Unknown registry @ns. | A dependency names a registry the project has not configured. Add it with registry add. |
Registry dependency cycle: ... | Items depend on each other in a loop. |
GitHub rate limit exceeded | Unauthenticated GitHub API limits are per IP; shared CI runners hit them. Retry after the reset. |
Registry constraints for <pkg> do not intersect. | Two items in one graph require incompatible versions of the same package. |
<item> is partially installed | Some of an item's files exist and some do not. For a recorded owner, use --diff or --overwrite. For unknown legacy source, restore the incomplete item from version control before adoption. |
<path> has unknown registry ownership | Verify provenance, then explicitly name the item and its existing dependencies with --adopt --diff, followed by --adopt. See legacy adoption. |
remix.ownership.json does not match the project prefix, preset, and UI path | Restore the matching configuration and source. Renaming or deleting installed files does not change the recorded context. |
Reference
- Open Code — what the CLI installs and why you own it.
remix.yamlschema 3,index.yamlschema 1, and catalog schema 2 are the stable contract. No otherremix.yamlschema is readable.