Guides · August 18, 2023
Publishing Your Own Component Registry: a Practical Guide
Creating a shadcn registry means publishing a registry.json manifest plus per-component JSON files that the shadcn CLI can fetch and install directly into a consumer's codebase. Here's how to structure, host, and version one properly.
By Polo Themes
Creating your own shadcn registry means publishing a small set of static JSON files — one manifest plus one entry per component — that describe source code, dependencies, and file targets in a schema the shadcn CLI already understands. Once those files are reachable over HTTPS, anyone can run npx shadcn add against your URL and get real, editable source dropped straight into their project, the same way they would from the official shadcn/ui registry. There is no build step, no npm package, and no bundler required on the consuming end — just a CLI that knows how to read your JSON and copy files into place.
This guide walks through the registry schema, a minimal working example, distribution options (static hosting vs. a live Next.js registry endpoint), private/team registries, and the versioning and maintenance habits that keep a registry usable once other people depend on it. It assumes you already have components you like and want to package — the shadcn CLI init flow itself is out of scope here.
Why a Registry Instead of an npm Package
The core idea behind shadcn/ui was never "install a component library" — it was "copy the source into your project so you own it." A registry is the distribution mechanism for that philosophy at scale. An npm package hides implementation behind a compiled bundle and a version number you have to keep in lockstep with peer dependencies; a registry hands the consumer actual .tsx files they can open, edit, and diff in their own git history from the moment they run the install command.
This matters more as teams build design systems around Tailwind and Radix primitives rather than around a single locked component API. A component registry lets an internal design-systems team publish "our button, our data table, our command palette" exactly the way shadcn/ui itself publishes its primitives — versionable, greppable, and forkable per-project without waiting on a package release to patch one component for one team.
The registry.json Schema, End to End
A registry has two layers of JSON. At the top, a single registry.json file lists every item your registry serves — name, type, and where to fetch its detail file. Each item then has its own JSON file (commonly served at a path like /r/button.json) containing the actual file contents, dependencies, and registry dependencies for that one component.
A minimal top-level manifest looks like this:
- name and homepage — identify the registry itself; shown by tooling and useful for attribution.
- items — an array, each with a name (e.g. `pricing-card`), a type (`registry:ui`, `registry:component`, `registry:block`, `registry:lib`, `registry:hook`, or `registry:page`), and enough metadata for the CLI to know what it is fetching.
The per-item JSON is where the real payload lives. Each file entry needs a path (relative source location in your repo), the content (the literal file text), and a type matching where the CLI should place it — a component under `components/ui`, a hook under `hooks`, a lib helper under `lib`, and so on. You also declare dependencies (plain npm packages the component needs, like `class-variance-authority` or `@radix-ui/react-dialog`) and registryDependencies — other items in your own registry (or even the public shadcn registry, referenced by name like `button`) that this component composes. The CLI resolves that dependency graph recursively, so a `data-table` item can depend on your `button` and `badge` items, which the installer pulls in automatically before writing the table itself.
`registry:block` is worth calling out separately from `registry:ui`: blocks are composed, page-level or section-level patterns (a full pricing section, a dashboard shell) built from multiple underlying components, and they can include their own cssVars and tailwind config fragments to extend the consumer's theme tokens rather than only dropping component files.
Building the Registry From Real Source Files
Hand-writing JSON with escaped source strings is a bad way to maintain this long-term. The practical pattern is to keep your components as normal .tsx files in a folder inside your repo, then run a small build script that reads each file, wraps it in the registry item schema, and writes the output JSON — either at build time in CI or via a Next.js route that generates it on demand.
The shadcn CLI ships a companion shadcn registry:build command in newer CLI versions specifically for this: point it at a registry.json that references your local source files by path, and it emits the fully resolved per-item JSON files into an output directory (commonly public/r/), ready to serve as static assets. This is the single biggest time-saver in the whole workflow — you keep authoring normal TypeScript components with real IDE support, linting, and type-checking, and the build step is what turns them into distributable JSON, not the other way around.
Hosting: Static Files vs. a Live Next.js Endpoint
Once you have the JSON, you have two realistic hosting shapes:
- Static hosting. Build the JSON files ahead of time and serve them from public/r/ on any static host or CDN — Vercel, Cloudflare Pages, S3 plus a CDN, or even GitHub Pages. This is the simplest, cheapest option and is the right default for a stable, versioned design system.
- A live Next.js route. Serve registry.json and each item from an API route or route handler that reads your component source at request time (or from a database) and returns the schema on the fly. This is worth the extra complexity only if items change per-request — for example, a multi-tenant registry where different teams see different component sets, or you want to gate access with auth headers before returning source.
Either way, set correct CORS and content-type headers (application/json) and make sure the endpoint is reachable without authentication if you intend it to be publicly installable — the CLI does a plain HTTPS fetch, so anything that requires a browser session or cookie-based auth will not work out of the box.
Private and Team-Only Registries
For an internal design system you don't want publicly installable, the CLI supports registries that require a bearer token or API key, configured per-registry in a consumer's components.json under a registries block, with the secret supplied via an environment variable rather than committed to source. Your server-side endpoint checks that header and returns a 401 for anyone without it. This is the shape most companies actually want: a registry that behaves like a private npm registry, gated to people on the team, but that still installs real source rather than a compiled package.
A lighter-weight private option, if your team is small and already inside one GitHub org, is to serve the JSON from a private repo's raw file URLs and gate access purely through git/GitHub permissions rather than a custom auth header — simpler to stand up, though less flexible if you later want per-team item visibility.
Versioning and Namespacing Without Breaking Consumers
Because installs copy code rather than pinning a resolvable semver range, "versioning" a registry means something different than versioning an npm package. A consumer who ran the install last month has a frozen snapshot of that component in their own repo; running the install again later fetches whatever your registry serves now, and there's no lockfile forcing consistency between the two. Treat this deliberately rather than letting it become a surprise:
- Namespace breaking changes as new items rather than silently rewriting an existing item's shape — ship `data-table-v2` alongside `data-table` instead of mutating `data-table` underneath people who already installed it.
- Keep a changelog per item, not just per repository, since consumers will ask "what changed in `command-palette` since March" far more often than "what changed in the whole registry."
- Pin registryDependencies to specific items, not ranges — since there's no semver resolution step, ambiguity here just becomes a runtime surprise for whoever installs later.
- Document the Tailwind and cssVars expectations per item explicitly, since a component that assumes theme tokens your consumer doesn't have will install cleanly and then look broken.
Validating the Registry Before Anyone Else Touches It
Before pointing teammates or the public at a registry URL, run the install against a disposable scratch project exactly the way an external consumer would — npx shadcn add https://your-domain.com/r/component-name.json — and check three things: the files land in the expected paths, the declared npm dependencies actually get installed, and the component renders without missing Tailwind classes or CSS variables. It's easy to build a registry item from source that compiles fine in your own repo, where the design tokens already exist, and only discover the missing pieces once someone installs it into a bare project.
It's also worth validating your registry.json against the official JSON Schema shadcn publishes for it — most editors will give you inline validation for free if you add a $schema reference at the top of the file, catching typos in type values or missing required fields before you ever run the CLI against them.
Where This Fits Into a Design-to-Code Workflow
A registry is the delivery mechanism, not the design decision. The components worth registry-fying are still the ones you've deliberately designed — spacing, states, and variants worked out in a design tool before a single line of Tailwind gets written. If you're assembling the source components for a registry from scratch, working from a structured Figma kit rather than freehanding markup tends to produce cleaner variant boundaries (the same default/hover/disabled/loading states a shadcn component needs map directly onto Figma component variants). Our Figma UI kits are built with that kind of variant structure in mind, even though translating a kit into registry JSON today is still a manual step on the engineering side rather than an automated export.
Registries also compose well with AI-assisted and agentic coding workflows: because each item is a small, well-described JSON object with explicit dependencies, an agent (or a human using an agent) can reason about "install `data-table`, which needs `button` and `badge`" far more reliably than it can reason about an arbitrary component library's undocumented internal imports. That predictability is a big part of why registries have become the preferred distribution shape for AI-native tooling, not just human developers running a CLI by hand.
Frequently Asked Questions
Do I need Next.js to host a shadcn registry?
No. A registry is just static JSON served over HTTPS with the right content type, so any static host works. Next.js (or another framework with route handlers) is only useful if you want items generated dynamically per request, such as per-tenant visibility or auth-gated content.
Can a private registry require authentication?
Yes. The shadcn CLI supports configuring a bearer token or API key per registry in the consumer's components.json, with the value supplied via an environment variable. Your server-side endpoint validates that header and returns items only to authorized requests.
What happens if I change a component after people have already installed it?
Nothing happens to code that's already been installed — it was copied into the consumer's repo, not linked or pinned. Anyone who runs the install again later gets whatever the registry currently serves. This is why namespacing breaking changes as new items, and keeping a changelog per item, matters more for registries than it does for a typical npm package.
Is a component registry the same thing as a Next.js starter template?
No. A starter template is a whole project scaffold you clone once at the beginning; a registry is an ongoing catalog of individual, independently installable pieces you can pull into an existing project at any point. The two are complementary — a starter can bootstrap a project, and a registry can keep supplying it with new components afterward.