BTST

Shadcn Registry

Eject and fully customize plugin UI components using the shadcn registry

Every BTST plugin ships its page components as a shadcn v4 registry block. This lets you eject the entire view layer into your own codebase and customize it freely — while all data-fetching, API logic, hooks, and routing stay untouched inside @btst/stack.

How it works

The registry approach separates two concerns:

LayerWhere it lives after ejection
View (page components, styles)Your repo at src/components/btst/{plugin}/client/
Data (hooks, API calls, routing)@btst/stack/plugins/{plugin}/client/hooks (npm package, unchanged)

Installing a registry block copies React component source files into your project. You own those files and can edit them however you like. The hooks they import remain in the npm package, so you always get data-layer bug fixes and new features via a normal npm update.

Install a plugin's UI

Pick the plugin you want to customize:

Or install a single plugin's UI directly:

# Blog
npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-blog.json

# AI Chat
npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-ai-chat.json

# CMS
npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-cms.json

# Form Builder
npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-form-builder.json

# UI Builder
npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-ui-builder.json

# Kanban
npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-kanban.json

# Comments
npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-comments.json

# Media
npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-media.json
# Blog
pnpx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-blog.json

# AI Chat
pnpx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-ai-chat.json

# CMS
pnpx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-cms.json

# Form Builder
pnpx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-form-builder.json

# UI Builder
pnpx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-ui-builder.json

# Kanban
pnpx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-kanban.json

# Comments
pnpx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-comments.json

# Media
pnpx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-media.json
# Blog
bunx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-blog.json

# AI Chat
bunx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-ai-chat.json

# CMS
bunx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-cms.json

# Form Builder
bunx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-form-builder.json

# UI Builder
bunx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-ui-builder.json

# Kanban
bunx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-kanban.json

# Comments
bunx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-comments.json

# Media
bunx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-media.json

Wire up ejected components

After the install, most plugins let you import your ejected components and pass them to the client plugin via pageComponents. Any key you omit falls back to the built-in default, so you only need to override the pages you actually want to change.

Blog

lib/stack-client.tsx
import { blogClientPlugin } from "@btst/stack/plugins/blog/client"
import { HomePageComponent } from "@/components/btst/blog/client/components/pages/home-page"
import { PostPageComponent } from "@/components/btst/blog/client/components/pages/post-page"

blogClientPlugin({
  apiBaseURL: "...",
  apiBasePath: "/api/data",
  siteBaseURL: "...",
  siteBasePath: "/pages",
  queryClient,
  pageComponents: {
    posts: HomePageComponent,    // published posts list
    post: PostPageComponent,     // single post detail
    // drafts | newPost | editPost | tag — omit to keep defaults
  },
})

AI Chat

lib/stack-client.tsx
import { aiChatClientPlugin } from "@btst/stack/plugins/ai-chat/client"
import { ChatPageComponent } from "@/components/btst/ai-chat/client/components/pages/chat-page"
import { ChatConversationPageComponent } from "@/components/btst/ai-chat/client/components/pages/chat-conversation-page"

aiChatClientPlugin({
  apiBaseURL: "...",
  apiBasePath: "/api/data",
  queryClient,
  pageComponents: {
    chat: ChatPageComponent,                         // chat home page
    chatConversation: ChatConversationPageComponent, // conversation page (authenticated mode)
  },
})

CMS

lib/stack-client.tsx
import { cmsClientPlugin } from "@btst/stack/plugins/cms/client"
import { DashboardPageComponent } from "@/components/btst/cms/client/components/pages/dashboard-page"
import { ContentListPageComponent } from "@/components/btst/cms/client/components/pages/content-list-page"

cmsClientPlugin({
  apiBaseURL: "...",
  apiBasePath: "/api/data",
  queryClient,
  pageComponents: {
    dashboard: DashboardPageComponent,      // CMS dashboard
    contentList: ContentListPageComponent,  // content list per type
    // newContent | editContent — omit to keep defaults
  },
})

Form Builder

lib/stack-client.tsx
import { formBuilderClientPlugin } from "@btst/stack/plugins/form-builder/client"
import { FormListPageComponent } from "@/components/btst/form-builder/client/components/pages/form-list-page"
import { EditFormPageComponent } from "@/components/btst/form-builder/client/components/pages/edit-form-page"

formBuilderClientPlugin({
  apiBaseURL: "...",
  apiBasePath: "/api/data",
  queryClient,
  pageComponents: {
    formList: FormListPageComponent, // form list page
    editForm: EditFormPageComponent, // form editor
    // newForm | submissions — omit to keep defaults
  },
})

UI Builder

lib/stack-client.tsx
import { uiBuilderClientPlugin } from "@btst/stack/plugins/ui-builder/client"
import { PageListPageComponent } from "@/components/btst/ui-builder/client/components/pages/page-list-page"
import { EditPagePageComponent } from "@/components/btst/ui-builder/client/components/pages/edit-page-page"

uiBuilderClientPlugin({
  apiBaseURL: "...",
  apiBasePath: "/api/data",
  queryClient,
  pageComponents: {
    pageList: PageListPageComponent, // page list
    editPage: EditPagePageComponent, // page builder editor
    // newPage — omit to keep default
  },
})

Kanban

lib/stack-client.tsx
import { kanbanClientPlugin } from "@btst/stack/plugins/kanban/client"
import { BoardsPageComponent } from "@/components/btst/kanban/client/components/pages/boards-page"
import { BoardPageComponent } from "@/components/btst/kanban/client/components/pages/board-page"

kanbanClientPlugin({
  apiBaseURL: "...",
  apiBasePath: "/api/data",
  queryClient,
  pageComponents: {
    boards: BoardsPageComponent, // boards list
    board: BoardPageComponent,   // board detail
    // newBoard — omit to keep default
  },
})

Comments

Comments is a little different: the ejected UI is typically rendered directly in your app rather than passed through a pageComponents option.

app/comments/moderation/page.tsx
import { ModerationPageComponent } from "@/components/btst/comments/client/components/pages/moderation-page"

export default function CommentsModerationPage() {
  return <ModerationPageComponent />
}

Keep commentsClientPlugin() registered and your comments overrides configured in StackProvider. The ejected page components will continue to use the shared hooks from @btst/stack/plugins/comments/client/hooks.

Media

Media uses a single pageComponents.library override for the media library page.

lib/stack-client.tsx
import { mediaClientPlugin } from "@btst/stack/plugins/media/client"
import { LibraryPageComponent } from "@/components/btst/media/client/components/pages/library-page"

mediaClientPlugin({
  apiBaseURL: "...",
  apiBasePath: "/api/data",
  siteBaseURL: "...",
  siteBasePath: "/pages",
  queryClient,
  pageComponents: {
    library: LibraryPageComponent, // media library page
  },
})

Keep your media overrides configured in StackProvider so the ejected component can resolve API config, upload mode, and hooks correctly.

Available pageComponents keys

The table below covers the plugins that currently support pageComponents overrides directly. Comments still use the direct-import pattern shown above.

PluginKeyPropsDescription
BlogpostsPublished posts list
BlogdraftsDrafts list
BlognewPostNew post editor
Blogpost{ slug: string }Single post detail
BlogeditPost{ slug: string }Edit post editor
Blogtag{ tagSlug: string }Tag + tagged posts
AI ChatchatChat home page
AI ChatchatConversation{ conversationId: string }Conversation detail
CMSdashboardCMS dashboard
CMScontentList{ typeSlug: string }Content list per type
CMSnewContent{ typeSlug: string }New content editor
CMSeditContent{ typeSlug: string; id: string }Edit content editor
Form BuilderformListForm list
Form BuildernewFormNew form editor
Form BuildereditForm{ id: string }Form editor
Form Buildersubmissions{ formId: string }Form submissions
UI BuilderpageListPage list
UI BuildernewPageNew page builder
UI BuildereditPage{ id: string }Page builder editor
KanbanboardsBoards list
KanbannewBoardNew board
Kanbanboard{ boardId: string }Board detail
MedialibraryMedia library page

What the registry installs

Hooks are never included. Components import hooks from @btst/stack/plugins/{plugin}/client/hooks. Only the view layer is ejected.

The shadcn CLI reads the registry JSON, resolves all dependencies, and copies source files into your project. The directory structure is preserved exactly, so all relative imports between components remain valid with no manual path-fixing required.

Standard shadcn components (Button, Dialog, etc.) referenced by the ejected files are listed as registryDependencies — the CLI installs them automatically.

Rebuilding the registry

The registry JSON files are generated from source. If you are working in the monorepo and have changed plugin components, regenerate them before committing:

pnpm --filter @btst/stack build-registry