opsinjs
RoadmapDecisions

ADR 0002. Distribute as a shadcn registry

Ship components as copyable source through a shadcn-spec registry, and do not build a CLI first.

Status

Accepted. 2026-09-02.

Context

A component library can be delivered three ways: as an npm package you import, as source you copy into your own repository, or as both. The choice is usually made on developer convenience. In this domain there are two stronger arguments.

Health products modify components. They do so always, not occasionally. The thresholds a range renders, the wording of a disclaimer, the exact phrasing of a status sentence: these are the parts a clinical safety review changes, and they are different for every product because the clinical context is different. A library that makes modification mean forking has misunderstood the job.

Copied source is auditable. A file in your repository, in your pull request, with your reviewer's name on it, is something you can put in a design-history file. A version range in package.json resolving to a tarball is harder to evidence, and the team that has to evidence it is the one buying this system.

The shadcn registry specification already describes exactly this distribution model, and npx shadcn add already implements the client. Both are public, stable and widely used.

Decision

Publish a shadcn-spec registry under /r on the canonical domain, register the @opsinjs namespace, and write no CLI of our own. Installation is npx shadcn add @opsinjs/<name>. The registry catalogue at /r/registry.json is also what makes the shadcn MCP server work against this project without any additional server.

Consequences

  • Consumers own the code. Upgrades are diffs, not version bumps, and nothing in a copied file records where it came from. Commit the files shadcn add writes, in their own commit, so a later --diff has something to compare against. Upgrades and diffs is the full procedure.
  • Fixes do not propagate. A security or safety fix reaches only the people who come back for it. Security advisories are therefore written for someone patching their own copy by hand, and that is a permanent cost of this decision rather than an oversight.
  • The MCP server works for free, because it consumes the same registry catalogue. So does /r/index.json, which carries status, category and aliases for agents that want the answer without the files.
  • Bundle size is a per-consumer question, not ours. The <BundleSize> block on a component page reports what the copied source costs, including whether it forces a client boundary. That is a normal integration question that almost no design system answers.
  • Not everything fits this model. The colour engine is a callable API with real algorithms in it. It derives a ramp and computes APCA, and copying that into a product is worse than importing it. Those ship as @opsinjs/* packages, and the Packages section documents them as an API. The registry is for components; the packages are for functions.

Alternatives considered

An npm package of components. Rejected: modification means forking, and forking loses the upgrade path entirely. It is also the wrong shape for a system whose most-changed parts are strings a clinician has to approve.

Both, from day one. Rejected as premature. No component exists yet, and two distribution paths mean two sets of documentation, two upgrade stories and twice the surface. The packages arrive when there is something in them worth importing.

Our own CLI, first. Rejected as building the shop before the stock. npx shadcn already does dependency resolution, path rewriting, diffing and dry runs. An opsinjs CLI is only justified by a job shadcn cannot do, and CLI reference says plainly which jobs those are. Theme validation is the likely first one.

Revisiting this

Revisit when a component's logic is complex enough that a consumer copying it cannot reasonably maintain it, or when the upstream registry specification changes in a way we would have to fork rather than follow.

Edit this page

Last read through against the system on 2026-09-02. Due for review every 12 months; expiry is reported by pnpm run check:freshness.

On this page