Skip to content
Join UI

Registry setup

Consume or self-host the registry.

Join UI ships a real shadcn-compatible registry: a JSON index plus one installable item per component, generated and validated during the build.

Consuming the registry

Add the namespace to components.json:

components.json
{
  "registries": {
    "@joinui": "https://ui.join-way.com/r/{name}.json"
  }
}

{name} is substituted by the CLI, so @joinui/<component> resolves to https://ui.join-way.com/r/<component>.json.

pnpm dlx shadcn@latest add @joinui/<component>

Or skip the namespace entirely:

bash
pnpm dlx shadcn@latest add https://ui.join-way.com/r/<component>.json

Endpoints

URLContents
/r/registry.jsonIndex of every item, without file bodies
/r/<name>.jsonA single installable item, with full source inlined

Both are static files served with Access-Control-Allow-Origin: *, so the CLI can read them from any project.

Anatomy of an item

Every item has the same shape. This is what one would look like for a hypothetical glow-card:

public/r/glow-card.json
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glow-card",
  "type": "registry:ui",
  "title": "Glow Card",
  "description": "A bounded panel that raises its rule on hover.",
  "author": "Join Way",
  "dependencies": [],
  "registryDependencies": ["utils"],
  "files": [
    {
      "path": "registry/components/glow-card.tsx",
      "type": "registry:ui",
      "target": "components/joinui/glow-card.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n…"
    }
  ],
  "categories": ["cards", "layout"],
  "docs": "https://ui.join-way.com/components/glow-card",
  "meta": { "status": "stable", "since": "2026-08-02", "featured": false }
}

path is where the file lives in this repository; target is where it lands in yours. meta is free-form — the CLI ignores it, and this site uses it to keep the published item self-describing.

Generation and validation

Items are never written by hand. pnpm registry:build projects them from the component metadata in lib/registry/components.ts:

bash
pnpm registry:build
text
✓ Registry built — 1 items, 13.2 kB of source
  registry.json
  public/r/registry.json
  public/r/*.json (1 files)

The counts track lib/registry/components.ts exactly — one entry there is one item here, and the byte count is the source inlined into the item files. The catalog is still short while the component set is rebuilt against the current design system, so expect those numbers to be small and to climb.

The build refuses to emit anything until validation passes:

bash
pnpm registry:validate

Validation covers the things a schema alone cannot:

  • metadata parses against a Zod schema (kebab-case slugs, PascalCase names, ISO dates, description length);
  • every file a component declares actually exists on disk;
  • every component has a preview registered in components/previews/registry.tsx;
  • related points only at slugs that exist, and never at itself;
  • the generated item still parses as a valid registry item, with non-empty file contents.

Self-hosting your own registry

The same pipeline works for a private design system.

Describe a component

One entry in lib/registry/components.ts. defineComponent derives the install command so the namespace lives in exactly one place.

lib/registry/components.ts
defineComponent({
  name: "GlowCard",
  slug: "glow-card",
  title: "Glow Card",
  description: "A card with an animated glow.",
  overview: "",
  category: "Cards",
  tags: ["card", "glow"],
  status: "new",
  featured: false,
  dependencies: [],
  registryDependencies: ["utils"],
  files: [uiFile("glow-card")],
  accessibility: [""],
  keyboard: [],
  props: [{ name: "GlowCard", props: [] }],
  usage: "",
  customization: [],
  related: [],
  since: "2026-08-02",
})

Build and host

bash
pnpm registry:build

Deploy the site. public/r/*.json is served as static JSON — any host will do, including a plain object store behind a CDN.

Point consumers at it

components.json
{
  "registries": {
    "@acme": "https://design.acme.com/r/{name}.json"
  }
}

Private registries

The CLI supports headers on a registry entry, so a token from the environment is enough to gate access:

components.json
{
  "registries": {
    "@acme": {
      "url": "https://design.acme.com/r/{name}.json",
      "headers": { "Authorization": "Bearer ${ACME_TOKEN}" }
    }
  }
}

Serve /r/* behind the check of your choice and drop the wildcard CORS header for those routes.

Registry file types

TypeUsed for
registry:uiA component — what every Join UI item is
registry:libA utility module
registry:hookA React hook
registry:componentA composed, app-level component
registry:fileAnything else, placed verbatim at target

Join UI keeps every component to a single registry:ui file on purpose: one install, one file to read, one file to delete.