Blog//6 min read
shadcn registry explained: how to ship your own components
What a shadcn registry is, how registry.json and registry-item.json work, how to host one on Next.js and install from it with npx shadcn add.
MSMarco Schulz, founder of Facade UI
The thing that makes shadcn different from a component library is that the code lands in your project. You run one command, a file appears in components/ui, and from that moment it is yours. Fewer people know that the same mechanism is open to anyone. A shadcn registry is just JSON on a URL, and the CLI will happily install from yours. Facade is built on that mechanism, and this article explains how it works so you can ship your own components the same way.
What registry.json is
A registry is a static index plus one JSON file per item. The index, registry.json, lists the items with their names, types and descriptions. It is what a human or a tool reads to find out what exists. Facade's lives at /r/registry.json.
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "facade",
"homepage": "https://facadeui.com",
"items": [
{
"name": "open-now-badge",
"type": "registry:ui",
"title": "Open now badge",
"description": "Live open or closed pill computed from weekly hours in the business's own time zone.",
"dependencies": ["motion"],
"registryDependencies": ["utils"],
"categories": ["local-business"],
"meta": { "tier": "free" },
"files": [
{
"path": "registry/items/open-now-badge/open-now-badge.tsx",
"type": "registry:ui",
"target": "components/facade/open-now-badge.tsx"
}
]
}
]
}The index is optional for installs. The CLI does not need it; it fetches the item file directly. But it is what lets a gallery page, an AI agent or another developer discover what the registry holds, so publish it.
The item schema
Each installable item is a registry-item.json document at its own URL. For Facade that is /r/<name>.json. The important fields:
name: the slug, used in install commands and as the file name.type:registry:uifor a single component,registry:blockfor a page section, plusregistry:hook,registry:lib,registry:styleand a few others. The type decides where the CLI puts the file by default.dependencies: npm packages to install. Facade items listmotionandlucide-react, nothing else.registryDependencies: other registry items this one needs."utils"resolves to the shadcncnhelper, so your component can import@/lib/utilsand the CLI makes sure it exists.files: an array of{ path, type, target, content }.contentis the full source as a string.targetis where it lands in the consumer's project; Facade targetscomponents/facade/<name>.tsxso the files never collide with the consumer's owncomponents/ui.cssVarsandcss: optional theme variables and CSS the item needs. Facade uses semantic tokens that shadcn already defines, plus a--brandpair documented once in the install docs, so the items ship withoutcssVars.
Two rules I learned the hard way. First, the content string is the source of truth; the path is informational. Keep the two in sync with a build script, never by hand. Second, the component must import only from packages in dependencies, from @/lib/utils, or from other items in registryDependencies. A stray import of a sibling file that is not in the registry installs cleanly and fails at build time.
Generating items from source
Hand writing JSON with embedded source strings does not survive the second edit. Facade keeps each item as a folder: registry/items/<slug>/<slug>.tsx for the component, demo.tsx for the usage example, and a small meta.json with title, description, category, tier and tags. A Node script walks the folders and emits everything else.
{
"name": "open-now-badge",
"title": "Open now badge",
"description": "Live open or closed pill computed from weekly hours in the business's own time zone.",
"type": "registry:ui",
"category": "Local business",
"tier": "free",
"dependencies": ["motion"],
"tags": ["hours", "status", "timezone", "restaurant", "storefront"]
}The script reads the imports from the source to confirm the dependency list, counts the lines, extracts the exported component names, and parses the export interface <Name>Props block so the docs page can render a props table without a separate doc file. The same run writes registry.json, every /r/<name>.json, a Markdown page per item and the llms.txt index. One source, several outputs, and no way for them to disagree.
Hosting on Next.js
The item files are static JSON, so any host that serves files works, including a public/ folder. Facade uses a route handler instead, for two reasons.
The first is licensing. Free items are served to anyone with cache headers. Pro items check the Authorization: Bearer <key> header against the license API and return 401 with a pointer to the setup docs when it is missing. The shadcn CLI supports per registry headers in components.json, which is how the @facade namespace works:
{
"registries": {
"@facade": {
"url": "https://facadeui.com/r/{name}.json",
"headers": { "Authorization": "Bearer ${FACADE_LICENSE_KEY}" }
}
}
}The {name} placeholder is replaced by the CLI, and ${FACADE_LICENSE_KEY} is read from the consumer's environment, so the key never lands in a committed file.
The second reason is counting. Every fetch of an item file is one install, because the CLI requests it exactly once per add. A route handler can record that number per item. A static file cannot.
Installing with npx shadcn add
From the consumer's side the command is the same one they already use for shadcn's own components, with a URL instead of a name:
npx shadcn@latest add https://facadeui.com/r/open-now-badge.json https://facadeui.com/r/opening-hours.jsonThe CLI fetches both files, installs motion if it is missing, makes sure @/lib/utils exists, and writes the two components to components/facade/. With the @facade registry configured, the same install is npx shadcn@latest add @facade/open-now-badge @facade/opening-hours, and Pro items work the same way.
After the install, the Open now badge and the Opening hours list are ordinary files in the project. Change the colors, rename a prop, delete the parts a client does not need. There is no version to upgrade and no breaking change to absorb; if a new version ships, the consumer decides whether to re-add it.
How Facade uses the registry with MCP
The registry index is also what the Facade MCP server reads. When an agent calls list_components it searches the same item list; get_component returns the Markdown docs with the source for free items, or for Pro items when the request carries a license header; get_install_command returns the npx shadcn add line for several items at once; and suggest_page returns an ordered block list for a business type. Because the MCP server, the docs pages, llms.txt and the CLI all read the same generated items, an agent sees exactly what a developer sees.
If you build your own registry, do the same. Generate the item JSON from source, publish the index, add a Markdown view per item, and the agents your users already run will find your components without any extra integration work.
FAQ
Do I need to publish an npm package?
No. A registry is JSON on a URL. The components ship as source, so there is no package to build, version or publish, and consumers get readable code instead of a compiled bundle. The only npm packages involved are the dependencies your components import.
Can a registry item include more than one file?
Yes. files is an array, and each entry has its own type and target. A block can ship a component, a hook and a data file together. Keep imports between those files relative so they resolve after installation.
How do I keep private items private?
Serve them from a route handler that checks a license header, and document the components.json registry entry with the ${ENV_VAR} placeholder so keys stay out of Git. The shadcn CLI sends the configured headers on every fetch, so no extra tooling is needed on the consumer's side.