---
title: "Self-hosting a fork"
description: "Running your own registry from a fork of opsinjs. What to change, what to keep, and how to stay able to take updates."
url: "https://opsinjs.pensievelabs.org/registry/self-hosting-a-fork"
source: "https://opsinjs.pensievelabs.org/registry/self-hosting-a-fork.md"
section: "Registry & distribution"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["fork", "internal design system", "self host", "run your own registry"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="guide" />

## Overview [#overview]

Forking is a first-class outcome here, not a failure of the project. A hospital
trust, an insurer or a company with its own clinical governance will have rules
opsinjs cannot encode, and the honest answer is to run your own registry with
your own components and your own review process.

This page is about doing that without ending up with a snapshot nobody can
update. The distinction that matters is between forking the **registry** and
forking the **system**. Forking the registry gives you your own items, your own
namespace and your own governance. Forking the system takes on the tokens, the
two axes and the doctrine. The first is cheap and expected. The second is
expensive and should be a decision somebody signs.

## Decide what you are forking [#decide-what-you-are-forking]

| You want                                       | Fork this                                           | Cost                                     |
| ---------------------------------------------- | --------------------------------------------------- | ---------------------------------------- |
| Your own components alongside opsinjs          | Nothing. Run a second registry                      | Low. Two namespaces in `components.json` |
| Your own theme                                 | Nothing. Publish a [preset](../theming/presets.mdx) | Low                                      |
| Your own copies of opsinjs components, edited  | The registry                                        | Medium. You own the diffs                |
| Different category ramps or a seventh category | The token source                                    | Medium. Regenerate and re-measure        |
| A different status vocabulary                  | The system                                          | High. See below                          |
| Different clinical doctrine                    | The system, and get clinical sign-off               | Highest                                  |

Most people who think they need the last two rows need the third.

<SafetyCallout severity="attention" evidence="opinion">
  Forking the status vocabulary breaks a contract that is not written in CSS. The
  four levels are shared by the components, this documentation, the registry
  metadata and every agent generating against it. A fork that renames or adds one
  will produce code, docs and tooling that quietly disagree. The disagreement
  concerns how urgent somebody's health data is. If you must,
  [Status palettes](../theming/status-palettes.mdx) documents how, and the answer
  includes writing it down where your successors will find it.
</SafetyCallout>

## Run your own registry [#run-your-own-registry]

You do not need a fork of this repository to do this. A registry is JSON at a
URL.

<Steps>
  ### Build a catalog [#build-a-catalog]

  `registry.json` with your `name`, `homepage` and `items`. Field by field in
  [registry.json](./registry-json.mdx). Keep it flat and keep file contents out of
  it.

  ### Serve one file per item [#serve-one-file-per-item]

  `/<name>.json`, matching the `{name}` pattern consumers will configure. The
  shadcn CLI has a `build` command that generates these from a local
  `registry.json`, so this is usually not code you write.

  ### Pick a namespace nobody else will want [#pick-a-namespace-nobody-else-will-want]

  Your organisation, not a generic word. `@acme-health`, not `@health`.

  ### Declare dependencies across registries by URL [#declare-dependencies-across-registries-by-url]

  An item of yours that depends on an opsinjs item must reference it by full URL,
  or by a namespace you can be certain the consumer has configured. A bare name
  resolves against whichever registry the CLI is currently looking at, and the
  failure is silent.

  ### Authenticate if it is private [#authenticate-if-it-is-private]

  The long form of a registry entry takes `headers`, with values substituted from
  the environment. Never commit a token into `components.json`.
</Steps>

## Fork the repository [#fork-the-repository]

If you need the token source, the generators and the documentation site, fork
the whole thing. Three things to do immediately, before anyone else clones it.

**Change the identity.** Registry `name`, `homepage`, the namespace, the preset
prefix and the site URL. A fork that still says `opsinjs.pensievelabs.org` will confuse your
own developers and misattribute your decisions to this project.
[Official resources](../project/official-resources.mdx) covers this from the
other direction, and the same reasoning applies to you.

**Keep the generators.** `build-tokens`, `check-contrast` and `assert-ia` are
the parts that make the system self-checking. A fork that keeps the tokens and
drops the checks has kept the values and lost the guarantee, and it will drift
within one release.

**Record the divergence.** One page in your own docs listing everything you
changed and why. This is the single highest-value artefact of a fork and it is
almost always the one that does not get written.

## Staying able to take updates [#staying-able-to-take-updates]

<Steps>
  ### Keep a clean upstream remote [#keep-a-clean-upstream-remote]

  Track this repository as a remote you never commit to. Merging is easier than
  archaeology.

  ### Keep your changes separable [#keep-your-changes-separable]

  Your own tokens in your own file; your own components under their own directory;
  your own documentation pages in their own group. A fork whose changes are
  scattered through generated files cannot be merged, only re-done.

  ### Never edit generated output [#never-edit-generated-output]

  `app/tokens.generated.css`, `lib/generated/**`, `registry/__index__.ts` and the
  generated reference pages are all rewritten by `pnpm run generate` and diffed by
  `pnpm run check:generated`. Edit the source.

  ### Re-measure after every merge [#re-measure-after-every-merge]

  ```bash
  pnpm run generate && pnpm run contrast && pnpm run check
  ```

  A merge that changes a token and not its measurement is the failure mode this
  whole pipeline exists to catch.
</Steps>

## Licensing [#licensing]

The code and the documentation are licensed separately. See
[Licence and attribution](../project/licence-and-attribution.mdx) before
publishing a fork, particularly if you are republishing the prose. Third-party
guidance this project cites, such as national health service manuals, is cited
and never copied, and that constraint travels with the fork: content under
Crown copyright or a similar licence cannot be redistributed just because it is
adjacent to something that can.

## Verify it worked [#verify-it-worked]

<Steps>
  ### A consumer can install from you with no configuration [#a-consumer-can-install-from-you-with-no-configuration]

  Test the full-URL form from a clean project, using an item that composes nothing.
  An item that names its siblings by namespace needs that namespace configured
  whichever form the consumer types, and many opsinjs items are in exactly
  that position. Because of that, either keep your leaf items self-contained, or
  make the namespace part of your published contract and say so on your own
  install page.

  ### Your catalog validates [#your-catalog-validates]

  Against shadcn's schema. A registry that only works with your own tooling has
  given up the main benefit of using somebody else's spec.

  ### The checks still pass [#the-checks-still-pass]

  `pnpm run check` in the fork. If `check:ia` fails, the fork's page tree and its
  catalogue have already diverged.
</Steps>

## Troubleshooting [#troubleshooting]

**Consumers get 404s for some items.** The catalog lists an item with no served
file. Run the build step that generates per-item files.

**A cross-registry dependency installs the wrong thing.** A bare name. Use a
URL.

**Merging upstream produces conflicts in generated files.** Take upstream
wholesale and regenerate; never hand-merge generated output.

**Our fork's contrast numbers do not match the published ones.** Correct, and
expected. Publish your own; the published figures describe the opsinjs presets
only.

## Next [#next]

* [Upgrades and diffs](./upgrades-and-diffs.mdx) is the consumer-side version of
  the same problem.
* [Contributing](../handbook/contributing/index.mdx) is for a change that
  belongs upstream instead.
