@@ -441,7 +751,7 @@ export function PluginCatalogCard({ href={decision.docsPath} className="rounded-sm outline-none hover:underline focus-visible:ring-2 focus-visible:ring-fd-primary" > - {plugin.label} + {copy.catalogTitle ?? plugin.label}
diff --git a/e2e/product-proof/capture.mjs b/e2e/product-proof/capture.mjs
index 041d8cea..778d435a 100644
--- a/e2e/product-proof/capture.mjs
+++ b/e2e/product-proof/capture.mjs
@@ -71,6 +71,31 @@ async function roundedScreenshot(input, width, height) {
.toBuffer();
}
+async function combineScreenshots(left, right) {
+ const [leftPanel, rightPanel] = await Promise.all(
+ [left, right].map((input) =>
+ sharp(input)
+ .resize(1120, 340, { fit: "cover", position: "top" })
+ .png()
+ .toBuffer(),
+ ),
+ );
+ return sharp({
+ create: {
+ width: 1120,
+ height: 700,
+ channels: 4,
+ background: palette.ink,
+ },
+ })
+ .composite([
+ { input: leftPanel, left: 0, top: 0 },
+ { input: rightPanel, left: 0, top: 360 },
+ ])
+ .png()
+ .toBuffer();
+}
+
async function writeWebP(file, base, composites, quality = 72) {
await sharp(base)
.composite(composites)
@@ -292,6 +317,78 @@ async function seedComments(request) {
}
}
+async function seedCms(request) {
+ const path = `/api/data/content/${seed.cms.typeSlug}`;
+ const response = await jsonRequest(
+ request,
+ "GET",
+ `${path}?limit=100&offset=0`,
+ );
+ const current = await response.json();
+ for (const item of current.items ?? []) {
+ if (seed.cms.records.some((record) => record.slug === item.slug)) {
+ await jsonRequest(request, "DELETE", `${path}/${item.id}`);
+ }
+ }
+ for (const record of seed.cms.records) {
+ await jsonRequest(request, "POST", path, record);
+ }
+ const persistedResponse = await jsonRequest(
+ request,
+ "GET",
+ `${path}?limit=100&offset=0`,
+ );
+ const persisted = await persistedResponse.json();
+ for (const record of seed.cms.records) {
+ const matches = (persisted.items ?? []).filter(
+ (item) =>
+ item.slug === record.slug && item.parsedData?.name === record.data.name,
+ );
+ if (matches.length !== 1) {
+ throw new Error(
+ `CMS fixture expected one persisted ${record.slug} record`,
+ );
+ }
+ }
+}
+
+async function seedKanban(request) {
+ const currentResponse = await jsonRequest(
+ request,
+ "GET",
+ `/api/data/boards?${new URLSearchParams({ slug: seed.kanban.slug, limit: "100" })}`,
+ );
+ const current = await currentResponse.json();
+ for (const board of current.items ?? []) {
+ await jsonRequest(request, "DELETE", `/api/data/boards/${board.id}`);
+ }
+ const createdResponse = await jsonRequest(
+ request,
+ "POST",
+ "/api/data/boards",
+ {
+ name: seed.kanban.name,
+ slug: seed.kanban.slug,
+ description: seed.kanban.description,
+ },
+ );
+ const board = await createdResponse.json();
+ if (!Array.isArray(board.columns) || board.columns.length < 3) {
+ throw new Error(
+ "Kanban fixture requires the three generated board columns",
+ );
+ }
+ for (const task of seed.kanban.tasks) {
+ await jsonRequest(request, "POST", "/api/data/tasks", {
+ title: task.title,
+ description: task.description,
+ priority: task.priority,
+ columnId: board.columns[task.column].id,
+ });
+ }
+ return board;
+}
+
async function cleanupMedia(request) {
const params = new URLSearchParams({
query: seed.media.uploadName,
@@ -499,6 +596,8 @@ async function main() {
try {
await seedBlog(context.request);
await seedComments(context.request);
+ await seedCms(context.request);
+ const kanbanBoard = await seedKanban(context.request);
await visit(page, "/pages/blog");
await page.getByRole("heading", { name: "Blog Posts" }).waitFor();
const blog = await screenshot(page, "blog.png");
@@ -518,11 +617,28 @@ async function main() {
.dragTo(page.getByTestId("form-builder-canvas"));
const form = await screenshot(page, "form-builder.png");
+ await visit(page, "/pages/cms");
+ await page.getByTestId("cms-dashboard-page").waitFor();
+ const cmsDashboard = await screenshot(page, "cms-dashboard.png");
+ await visit(page, `/pages/cms/${seed.cms.typeSlug}`);
+ await page.getByText(seed.cms.records[0].slug, { exact: true }).waitFor();
+ const cmsRecords = await screenshot(page, "cms-records.png");
+ const cms = await combineScreenshots(cmsDashboard, cmsRecords);
+
const uiBuilderPage = await seedUiBuilder(context.request);
await visit(page, `/pages/ui-builder/${uiBuilderPage.id}/edit`);
await page.getByRole("heading", { name: "Component Properties" }).waitFor();
const uiBuilder = await screenshot(page, "ui-builder.png");
+ await visit(page, `/pages/kanban/${kanbanBoard.id}`);
+ await page.getByText(seed.kanban.name, { exact: true }).waitFor();
+ const kanban = await screenshot(page, "kanban.png");
+
+ await visit(page, "/pages/comments/moderation");
+ await page.getByTestId("tab-approved").click();
+ await page.getByText(seed.comments[0].body, { exact: true }).waitFor();
+ const comments = await screenshot(page, "comments.png");
+
await seedMedia(context.request);
await visit(page, seed.media.libraryPath);
await page.getByPlaceholder(seed.media.expectedControl).waitFor();
@@ -538,12 +654,14 @@ async function main() {
`Media library expected one visible ${seed.media.uploadName} card`,
);
}
+ const media = await screenshot(page, "media.png");
await visit(page, seed.routeDocs.pagePath);
await page
.getByText(seed.routeDocs.expectedTitle, { exact: false })
.first()
.waitFor();
+ const routeDocs = await screenshot(page, "route-docs.png");
await visit(page, seed.openApi.referencePath);
await page
@@ -584,6 +702,17 @@ async function main() {
result: "Editable form + live preview",
label: "FORM BUILDER / AUTHENTIC GENERATED APP",
});
+ await proofFrame("cms-proof.webp", cms, {
+ eyebrow: "Schema-to-operations proof",
+ title: ["Define content.", "Give editors", "a workflow."],
+ body: [
+ "The dashboard reflects types",
+ "defined in code; the list shows",
+ "records stored by the same app.",
+ ],
+ result: "Content model + managed records",
+ label: "CMS / TWO AUTHENTIC WORKFLOW STATES",
+ });
await proofFrame("ui-builder-proof.webp", uiBuilder, {
eyebrow: "Complex UI proof",
title: ["Compose pages.", "Keep the code."],
@@ -595,6 +724,50 @@ async function main() {
result: "Published page composition",
label: "UI BUILDER / AUTHENTIC GENERATED APP",
});
+ await proofFrame("kanban-proof.webp", kanban, {
+ eyebrow: "Workflow state proof",
+ title: ["Move work", "through", "your app."],
+ body: [
+ "The generated board holds",
+ "columns, priorities, and tasks",
+ "in the adopter's database.",
+ ],
+ result: "Board + columns + task state",
+ label: "KANBAN / AUTHENTIC GENERATED APP",
+ });
+ await proofFrame("comments-proof.webp", comments, {
+ eyebrow: "Moderation proof",
+ title: ["Discussion", "stays with", "the resource."],
+ body: [
+ "A seeded resource comment",
+ "appears in the shipped",
+ "moderation workflow.",
+ ],
+ result: "Resource context + moderation",
+ label: "COMMENTS / AUTHENTIC GENERATED APP",
+ });
+ await proofFrame("media-proof.webp", media, {
+ eyebrow: "Storage-to-library proof",
+ title: ["Upload once.", "Reuse the asset."],
+ body: [
+ "A checked-in fixture moves",
+ "through the real upload API",
+ "into the generated library.",
+ ],
+ result: "Uploaded file + stored metadata",
+ label: "MEDIA / AUTHENTIC GENERATED APP",
+ });
+ await proofFrame("route-docs-proof.webp", routeDocs, {
+ eyebrow: "Registered-route proof",
+ title: ["See routes", "your stack", "composed."],
+ body: [
+ "The reference is generated",
+ "from actual client plugins,",
+ "parameters, and sitemaps.",
+ ],
+ result: "Route + plugin + parameter context",
+ label: "ROUTE DOCS / AUTHENTIC GENERATED APP",
+ });
await proofFrame("openapi-proof.webp", openapi, {
eyebrow: "One-sided plugin proof",
title: ["OpenAPI needs", "no client half."],
diff --git a/e2e/product-proof/dogfood-data.json b/e2e/product-proof/dogfood-data.json
index c1996724..ae8dbbee 100644
--- a/e2e/product-proof/dogfood-data.json
+++ b/e2e/product-proof/dogfood-data.json
@@ -33,6 +33,31 @@
"name": "Plugin evaluation",
"slug": "plugin-evaluation"
},
+ "cms": {
+ "typeSlug": "product",
+ "records": [
+ {
+ "slug": "btst-release-evidence",
+ "data": {
+ "name": "Release evidence kit",
+ "description": "A typed content record managed through the generated CMS surface.",
+ "price": 49,
+ "featured": true,
+ "category": "Electronics"
+ }
+ },
+ {
+ "slug": "btst-plugin-catalog",
+ "data": {
+ "name": "Plugin catalog",
+ "description": "A second record that proves the generated list is backed by stored content.",
+ "price": 29,
+ "featured": false,
+ "category": "Electronics"
+ }
+ }
+ ]
+ },
"comments": [
{
"resourceId": "shipping-plugin-catalog",
@@ -41,6 +66,31 @@
"status": "approved"
}
],
+ "kanban": {
+ "name": "BTST Release Board",
+ "slug": "btst-release-board",
+ "description": "A generated board backed by the registered Kanban plugin.",
+ "tasks": [
+ {
+ "title": "Verify plugin boundaries",
+ "description": "Confirm what BTST supplies and what the application owns.",
+ "priority": "HIGH",
+ "column": 0
+ },
+ {
+ "title": "Review generated routes",
+ "description": "Inspect the generated product surfaces before release.",
+ "priority": "MEDIUM",
+ "column": 1
+ },
+ {
+ "title": "Capture release evidence",
+ "description": "Keep the checked-in proof tied to the current implementation.",
+ "priority": "LOW",
+ "column": 2
+ }
+ ]
+ },
"media": {
"libraryPath": "/pages/media",
"fixture": "../fixtures/test-image.png",
diff --git a/packages/cli/plugin-decisions.json b/packages/cli/plugin-decisions.json
index c6c7c2ac..6270f009 100644
--- a/packages/cli/plugin-decisions.json
+++ b/packages/cli/plugin-decisions.json
@@ -27,6 +27,66 @@
"demoPath": "https://www.better-stack.ai/p/blog",
"sourcePath": "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/blog"
},
+ "ai-chat": {
+ "topology": "Full-stack",
+ "releaseStatus": "Preview",
+ "supplies": [
+ "Streaming chat APIs with typed tool, attachment, and lifecycle boundaries",
+ "Conversation and message models for authenticated history",
+ "SSR-aware conversation list and chat routes",
+ "Customizable chat pages, hooks, and prompt UI"
+ ],
+ "adopterSupplies": [
+ "An AI SDK model provider, credentials, usage policy, and provider billing",
+ "A database adapter with isolated transactions for authenticated history",
+ "Authorization rules for authenticated access, tools, and attachments",
+ "An upload implementation when file attachments are enabled"
+ ],
+ "dependencies": [
+ "An AI SDK language model",
+ "A database adapter with isolated transaction support for authenticated persistence"
+ ],
+ "externalServices": [
+ "The adopter-selected AI model provider receives prompts and generates responses"
+ ],
+ "supportedFrameworks": [
+ "Next.js 15+ App Router",
+ "React Router v7",
+ "TanStack Start"
+ ],
+ "docsPath": "/plugins/ai-chat",
+ "demoPath": "https://www.better-stack.ai/playground?plugins=ai-chat&view=preview",
+ "sourcePath": "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/ai-chat"
+ },
+ "cms": {
+ "topology": "Full-stack",
+ "releaseStatus": "Preview",
+ "supplies": [
+ "Content-type and content-item data models with typed CRUD APIs and lifecycle hooks",
+ "Admin routes for content-type lists, entries, creation, and editing",
+ "Schema-driven forms generated from adopter-defined Zod content types",
+ "Client hooks plus customizable and ejectable admin pages"
+ ],
+ "adopterSupplies": [
+ "Code-defined Zod content types and application-owned public rendering",
+ "A BTST database adapter",
+ "An image upload implementation when file fields are enabled",
+ "Authorization rules when content operations are protected"
+ ],
+ "dependencies": [
+ "A BTST database adapter",
+ "Code-defined Zod content types"
+ ],
+ "externalServices": [],
+ "supportedFrameworks": [
+ "Next.js 15+ App Router",
+ "React Router v7",
+ "TanStack Start"
+ ],
+ "docsPath": "/plugins/cms",
+ "demoPath": "https://www.better-stack.ai/playground?plugins=cms&view=preview",
+ "sourcePath": "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/cms"
+ },
"form-builder": {
"topology": "Full-stack",
"releaseStatus": "Preview",
@@ -52,6 +112,142 @@
"docsPath": "/plugins/form-builder",
"sourcePath": "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/form-builder"
},
+ "ui-builder": {
+ "topology": "Client-only",
+ "relationship": "Dependent",
+ "releaseStatus": "Preview",
+ "supplies": [
+ "Visual page-list, creation, and editing routes",
+ "A component registry, layer editor, variables, and reusable block support",
+ "A PageRenderer for application-owned public routes",
+ "A CMS content-type declaration for storing page layers and status"
+ ],
+ "adopterSupplies": [
+ "The components and blocks editors may place on a page",
+ "A public route that loads CMS page data and mounts PageRenderer",
+ "CMS authorization rules for page records",
+ "The application shell and deployment"
+ ],
+ "dependencies": ["The CMS plugin, added automatically by the CLI"],
+ "externalServices": [],
+ "supportedFrameworks": [
+ "Next.js 15+ App Router",
+ "React Router v7",
+ "TanStack Start"
+ ],
+ "docsPath": "/plugins/ui-builder",
+ "demoPath": "https://www.better-stack.ai/playground?plugins=ui-builder&view=preview",
+ "sourcePath": "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/ui-builder"
+ },
+ "kanban": {
+ "topology": "Full-stack",
+ "releaseStatus": "Preview",
+ "supplies": [
+ "Board, column, task, and assignee data models with typed APIs and lifecycle hooks",
+ "SSR-aware board list, creation, and detail routes",
+ "Drag-and-drop column and task workflows with priority and assignee UI",
+ "Customizable hooks and ejectable Kanban pages"
+ ],
+ "adopterSupplies": [
+ "A database adapter with isolated transaction support for persistent writes",
+ "Authorization rules plus user search and identity resolution when assignees are enabled",
+ "Product-specific workflow rules through configuration and lifecycle hooks",
+ "The application shell and deployment"
+ ],
+ "dependencies": ["A database adapter with isolated transaction support"],
+ "externalServices": [],
+ "supportedFrameworks": [
+ "Next.js 15+ App Router",
+ "React Router v7",
+ "TanStack Start"
+ ],
+ "docsPath": "/plugins/kanban",
+ "demoPath": "https://www.better-stack.ai/playground?plugins=kanban&view=preview",
+ "sourcePath": "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/kanban"
+ },
+ "comments": {
+ "topology": "Full-stack",
+ "releaseStatus": "Preview",
+ "supplies": [
+ "Threaded comment and reaction data models with typed APIs and lifecycle hooks",
+ "Embeddable CommentThread and CommentCount components",
+ "A moderation route for pending, approved, and spam comments",
+ "Customizable hooks and ejectable moderation UI"
+ ],
+ "adopterSupplies": [
+ "A BTST database adapter",
+ "The resource type and identifier that each thread belongs to",
+ "Authorization rules and authoritative request identity when access is protected",
+ "A user resolver when author names and avatars should be displayed"
+ ],
+ "dependencies": [
+ "A BTST database adapter",
+ "An adopter-owned host resource"
+ ],
+ "externalServices": [],
+ "supportedFrameworks": [
+ "Next.js 15+ App Router",
+ "React Router v7",
+ "TanStack Start"
+ ],
+ "docsPath": "/plugins/comments",
+ "sourcePath": "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/comments"
+ },
+ "media": {
+ "topology": "Full-stack",
+ "releaseStatus": "Preview",
+ "supplies": [
+ "Asset and folder data models with typed upload, registration, and library APIs",
+ "An SSR-aware media-library route with search, folders, and asset actions",
+ "Embeddable MediaPicker and ImageInputField components",
+ "Local, S3-compatible, and Vercel Blob storage adapter implementations"
+ ],
+ "adopterSupplies": [
+ "A database adapter with isolated transaction support for persistent writes",
+ "A configured storage adapter and its credentials or local upload directory",
+ "Allowed MIME types, size limits, URL prefixes, and authorization policy",
+ "The application routes and fields that embed the picker or image input"
+ ],
+ "dependencies": [
+ "An isolating Prisma, Drizzle, or Kysely database adapter for persistent writes",
+ "A configured media storage adapter"
+ ],
+ "externalServices": [
+ "Optional S3-compatible storage or Vercel Blob when the adopter selects those adapters"
+ ],
+ "supportedFrameworks": [
+ "Next.js 15+ App Router",
+ "React Router v7",
+ "TanStack Start"
+ ],
+ "docsPath": "/plugins/media",
+ "sourcePath": "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/media"
+ },
+ "route-docs": {
+ "topology": "Client-only",
+ "releaseStatus": "Preview",
+ "supplies": [
+ "A generated route-reference page for registered BTST client plugins",
+ "Route paths, path and query parameters, sitemap entries, and plugin context",
+ "Parameter-aware navigation to routes in the adopter's application",
+ "An ejectable Route Docs page over the packaged introspection runtime"
+ ],
+ "adopterSupplies": [
+ "Registered BTST client plugins whose routes can be inspected",
+ "A deployment-level access boundary when route details should be private",
+ "Concrete parameter values before navigating to a dynamic route",
+ "The application shell and resolved site location"
+ ],
+ "dependencies": ["Registered BTST client routes to inspect"],
+ "externalServices": [],
+ "supportedFrameworks": [
+ "Next.js 15+ App Router",
+ "React Router v7",
+ "TanStack Start"
+ ],
+ "docsPath": "/plugins/route-docs",
+ "sourcePath": "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/route-docs"
+ },
"open-api": {
"topology": "Backend-only",
"releaseStatus": "Preview",
@@ -78,6 +274,37 @@
],
"docsPath": "/plugins/open-api",
"sourcePath": "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/open-api"
+ },
+ "better-auth-ui": {
+ "topology": "Client-only",
+ "relationship": "Companion",
+ "releaseStatus": "Preview",
+ "supplies": [
+ "Auth and account route definitions backed by @btst/better-auth-ui",
+ "Sign-in, sign-up, recovery, account, security, and settings UI",
+ "A CLI scaffold that creates one browser client for the existing auth endpoint",
+ "Framework-native session refresh wiring for the maintained integration paths"
+ ],
+ "adopterSupplies": [
+ "An existing Better Auth server, schema, migrations, providers, and secrets",
+ "A Better Auth browser client configured for the adopter-owned endpoint",
+ "Any optional Better Auth server and client plugins used by the application",
+ "Deployment, session policy, and separate BTST authorization mapping when needed"
+ ],
+ "dependencies": [
+ "An existing Better Auth backend and browser client",
+ "The supported @btst/better-auth-ui and Better Auth package cohort"
+ ],
+ "externalServices": [
+ "The adopter's existing Better Auth endpoint; BTST does not host it"
+ ],
+ "supportedFrameworks": [
+ "Next.js 15+ App Router",
+ "React Router v7",
+ "TanStack Start"
+ ],
+ "docsPath": "/plugins/better-auth-ui",
+ "sourcePath": "https://github.com/better-stack-ai/better-auth-ui"
}
}
}
diff --git a/packages/cli/src/utils/__tests__/plugin-decision-manifest.test.ts b/packages/cli/src/utils/__tests__/plugin-decision-manifest.test.ts
index b417dfea..8280c104 100644
--- a/packages/cli/src/utils/__tests__/plugin-decision-manifest.test.ts
+++ b/packages/cli/src/utils/__tests__/plugin-decision-manifest.test.ts
@@ -25,8 +25,16 @@ describe("published plugin decision manifest", () => {
expect(manifest).toEqual({ schemaVersion: 1, plugins: PLUGIN_DECISIONS });
expect(Object.keys(manifest.plugins)).toEqual([
"blog",
+ "ai-chat",
+ "cms",
"form-builder",
+ "ui-builder",
+ "kanban",
+ "comments",
+ "media",
+ "route-docs",
"open-api",
+ "better-auth-ui",
]);
await expect(
execFileAsync(process.execPath, [tsxCli, generatorPath, "--check"]),
diff --git a/packages/cli/src/utils/__tests__/plugin-meta.test.ts b/packages/cli/src/utils/__tests__/plugin-meta.test.ts
index dc2884eb..ba8389fd 100644
--- a/packages/cli/src/utils/__tests__/plugin-meta.test.ts
+++ b/packages/cli/src/utils/__tests__/plugin-meta.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { PLUGINS } from "../constants";
+import type { PluginKey } from "../../types";
const maintainedFrameworks = [
"Next.js 15+ App Router",
@@ -7,23 +8,39 @@ const maintainedFrameworks = [
"TanStack Start",
] as const;
-function representativePlugin(key: "blog" | "form-builder" | "open-api") {
+const releasedPluginKeys = [
+ "blog",
+ "ai-chat",
+ "cms",
+ "form-builder",
+ "ui-builder",
+ "kanban",
+ "comments",
+ "media",
+ "route-docs",
+ "open-api",
+ "better-auth-ui",
+] as const satisfies readonly PluginKey[];
+
+function releasedPlugin(key: PluginKey) {
const plugin = PLUGINS.find((candidate) => candidate.key === key);
if (!plugin) throw new Error(`Missing plugin metadata for ${key}`);
if (!plugin.decision) throw new Error(`Missing decision metadata for ${key}`);
return { plugin, decision: plugin.decision };
}
-describe("representative plugin decision metadata", () => {
- it.each(["blog", "form-builder", "open-api"] as const)(
+describe("released plugin decision metadata", () => {
+ it.each(releasedPluginKeys)(
"publishes the shared evaluator contract for %s",
(key) => {
- const { decision } = representativePlugin(key);
+ const { decision } = releasedPlugin(key);
expect(decision.releaseStatus).toBe("Preview");
expect(decision.supportedFrameworks).toEqual(maintainedFrameworks);
expect(decision.docsPath).toBe(`/plugins/${key}`);
- expect(decision.sourcePath).toContain(`/plugins/${key}`);
+ expect(decision.sourcePath).toMatch(
+ /^https:\/\/github\.com\/better-stack-ai\//,
+ );
expect(decision.supplies.length).toBeGreaterThan(0);
expect(decision.adopterSupplies.length).toBeGreaterThan(0);
expect(decision).not.toHaveProperty("audience");
@@ -33,8 +50,15 @@ describe("representative plugin decision metadata", () => {
},
);
+ it("covers the released CLI inventory without roadmap records", () => {
+ expect(PLUGINS.map((plugin) => plugin.key)).toEqual(releasedPluginKeys);
+ expect(
+ PLUGINS.filter((plugin) => plugin.decision).map((plugin) => plugin.key),
+ ).toEqual(releasedPluginKeys);
+ });
+
it("describes Blog as the complete feature proof with a working live result", () => {
- const { plugin, decision } = representativePlugin("blog");
+ const { plugin, decision } = releasedPlugin("blog");
expect(decision.topology).toBe("Full-stack");
expect(plugin.backendImportPath).toBeDefined();
@@ -46,7 +70,7 @@ describe("representative plugin decision metadata", () => {
});
it("states the complete Form Builder data workflow without inventing a demo", () => {
- const { plugin, decision } = representativePlugin("form-builder");
+ const { plugin, decision } = releasedPlugin("form-builder");
expect(decision.topology).toBe("Full-stack");
expect(plugin.backendImportPath).toBeDefined();
@@ -64,7 +88,7 @@ describe("representative plugin decision metadata", () => {
});
it("keeps OpenAPI backend-only and Scalar optional", () => {
- const { plugin, decision } = representativePlugin("open-api");
+ const { plugin, decision } = releasedPlugin("open-api");
expect(decision.topology).toBe("Backend-only");
expect(plugin.backendImportPath).toBeDefined();
@@ -80,4 +104,80 @@ describe("representative plugin decision metadata", () => {
"The optional Scalar reference loads @scalar/api-reference from jsDelivr",
]);
});
+
+ it("states the AI Chat model, persistence, and ownership boundaries", () => {
+ const { plugin, decision } = releasedPlugin("ai-chat");
+
+ expect(decision.topology).toBe("Full-stack");
+ expect(plugin.backendImportPath).toBeDefined();
+ expect(plugin.clientImportPath).toBeDefined();
+ expect(decision.demoPath).toBe(
+ "https://www.better-stack.ai/playground?plugins=ai-chat&view=preview",
+ );
+ expect(decision.dependencies).toContain(
+ "A database adapter with isolated transaction support for authenticated persistence",
+ );
+ expect(decision.adopterSupplies).toContain(
+ "An AI SDK model provider, credentials, usage policy, and provider billing",
+ );
+ });
+
+ it("separates CMS content modeling from application-owned public rendering", () => {
+ const { decision } = releasedPlugin("cms");
+
+ expect(decision.topology).toBe("Full-stack");
+ expect(decision.demoPath).toBe(
+ "https://www.better-stack.ai/playground?plugins=cms&view=preview",
+ );
+ expect(decision.adopterSupplies).toContain(
+ "Code-defined Zod content types and application-owned public rendering",
+ );
+ });
+
+ it("keeps UI Builder client-only and dependent on CMS", () => {
+ const { plugin, decision } = releasedPlugin("ui-builder");
+
+ expect(decision.topology).toBe("Client-only");
+ expect(decision.relationship).toBe("Dependent");
+ expect(plugin.clientImportPath).toBeDefined();
+ expect(decision.dependencies).toContain(
+ "The CMS plugin, added automatically by the CLI",
+ );
+ expect(decision.demoPath).toBe(
+ "https://www.better-stack.ai/playground?plugins=ui-builder&view=preview",
+ );
+ });
+
+ it("keeps Kanban full-stack while leaving identities and policy to the app", () => {
+ const { decision } = releasedPlugin("kanban");
+
+ expect(decision.topology).toBe("Full-stack");
+ expect(decision.demoPath).toBe(
+ "https://www.better-stack.ai/playground?plugins=kanban&view=preview",
+ );
+ expect(decision.adopterSupplies).toContain(
+ "Authorization rules plus user search and identity resolution when assignees are enabled",
+ );
+ });
+
+ it("does not invent standalone demos for embedded or infrastructure plugins", () => {
+ for (const key of ["comments", "media", "route-docs"] as const) {
+ expect(releasedPlugin(key).decision.demoPath).toBeUndefined();
+ }
+ expect(releasedPlugin("comments").decision.topology).toBe("Full-stack");
+ expect(releasedPlugin("media").decision.topology).toBe("Full-stack");
+ expect(releasedPlugin("route-docs").decision.topology).toBe("Client-only");
+ });
+
+ it("describes Better Auth UI as a client-only companion, not an auth backend", () => {
+ const { plugin, decision } = releasedPlugin("better-auth-ui");
+
+ expect(decision.topology).toBe("Client-only");
+ expect(decision.relationship).toBe("Companion");
+ expect(plugin.backendImportPath).toBeUndefined();
+ expect(decision.dependencies).toContain(
+ "An existing Better Auth backend and browser client",
+ );
+ expect(decision.demoPath).toBeUndefined();
+ });
});
diff --git a/packages/cli/src/utils/constants.ts b/packages/cli/src/utils/constants.ts
index 69e5f113..9d3031e5 100644
--- a/packages/cli/src/utils/constants.ts
+++ b/packages/cli/src/utils/constants.ts
@@ -94,6 +94,7 @@ export const PLUGINS: readonly PluginMeta[] = [
clientSymbol: "aiChatClientPlugin",
configKey: "aiChat",
extraPackages: ["@ai-sdk/openai", "ai"],
+ decision: PLUGIN_DECISIONS["ai-chat"],
},
{
key: "cms",
@@ -105,6 +106,7 @@ export const PLUGINS: readonly PluginMeta[] = [
clientSymbol: "cmsClientPlugin",
configKey: "cms",
hasSeedData: true,
+ decision: PLUGIN_DECISIONS.cms,
},
{
key: "form-builder",
@@ -128,6 +130,7 @@ export const PLUGINS: readonly PluginMeta[] = [
clientSymbol: "uiBuilderClientPlugin",
configKey: "uiBuilder",
hasSeedData: true,
+ decision: PLUGIN_DECISIONS["ui-builder"],
},
{
key: "kanban",
@@ -139,6 +142,7 @@ export const PLUGINS: readonly PluginMeta[] = [
clientSymbol: "kanbanClientPlugin",
configKey: "kanban",
hasSeedData: true,
+ decision: PLUGIN_DECISIONS.kanban,
},
{
key: "comments",
@@ -149,6 +153,7 @@ export const PLUGINS: readonly PluginMeta[] = [
clientImportPath: "@btst/stack/plugins/comments/client",
clientSymbol: "commentsClientPlugin",
configKey: "comments",
+ decision: PLUGIN_DECISIONS.comments,
},
{
key: "media",
@@ -162,6 +167,7 @@ export const PLUGINS: readonly PluginMeta[] = [
// Without it installed, Next.js/webpack fails to resolve the dynamic import
// even though the code path is never reached when using other storage adapters.
extraPackages: ["@vercel/blob"],
+ decision: PLUGIN_DECISIONS.media,
},
{
key: "route-docs",
@@ -170,6 +176,7 @@ export const PLUGINS: readonly PluginMeta[] = [
clientImportPath: "@btst/stack/plugins/route-docs/client",
clientSymbol: "routeDocsClientPlugin",
configKey: "routeDocs",
+ decision: PLUGIN_DECISIONS["route-docs"],
},
{
key: "open-api",
@@ -208,6 +215,7 @@ export const PLUGINS: readonly PluginMeta[] = [
"@better-auth/api-key@1.6.16",
"@better-auth/passkey@1.6.16",
],
+ decision: PLUGIN_DECISIONS["better-auth-ui"],
},
];
diff --git a/packages/cli/src/utils/plugin-decision.ts b/packages/cli/src/utils/plugin-decision.ts
index a7c5fed5..bc36f94d 100644
--- a/packages/cli/src/utils/plugin-decision.ts
+++ b/packages/cli/src/utils/plugin-decision.ts
@@ -42,14 +42,9 @@ export interface PluginDecisionMeta {
/** Canonical working demo identifier, when one exists. */
demoPath?: `https://${string}`;
/** Canonical public source identifier. */
- sourcePath: `https://github.com/better-stack-ai/better-stack/${string}`;
+ sourcePath: `https://github.com/better-stack-ai/${string}`;
}
-type RepresentativePluginKey = Extract<
- PluginKey,
- "blog" | "form-builder" | "open-api"
->;
-
const MAINTAINED_FRAMEWORKS = [
"Next.js 15+ App Router",
"React Router v7",
@@ -80,6 +75,58 @@ export const PLUGIN_DECISIONS = {
sourcePath:
"https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/blog",
},
+ "ai-chat": {
+ topology: "Full-stack",
+ releaseStatus: "Preview",
+ supplies: [
+ "Streaming chat APIs with typed tool, attachment, and lifecycle boundaries",
+ "Conversation and message models for authenticated history",
+ "SSR-aware conversation list and chat routes",
+ "Customizable chat pages, hooks, and prompt UI",
+ ],
+ adopterSupplies: [
+ "An AI SDK model provider, credentials, usage policy, and provider billing",
+ "A database adapter with isolated transactions for authenticated history",
+ "Authorization rules for authenticated access, tools, and attachments",
+ "An upload implementation when file attachments are enabled",
+ ],
+ dependencies: [
+ "An AI SDK language model",
+ "A database adapter with isolated transaction support for authenticated persistence",
+ ],
+ externalServices: [
+ "The adopter-selected AI model provider receives prompts and generates responses",
+ ],
+ supportedFrameworks: MAINTAINED_FRAMEWORKS,
+ docsPath: "/plugins/ai-chat",
+ demoPath:
+ "https://www.better-stack.ai/playground?plugins=ai-chat&view=preview",
+ sourcePath:
+ "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/ai-chat",
+ },
+ cms: {
+ topology: "Full-stack",
+ releaseStatus: "Preview",
+ supplies: [
+ "Content-type and content-item data models with typed CRUD APIs and lifecycle hooks",
+ "Admin routes for content-type lists, entries, creation, and editing",
+ "Schema-driven forms generated from adopter-defined Zod content types",
+ "Client hooks plus customizable and ejectable admin pages",
+ ],
+ adopterSupplies: [
+ "Code-defined Zod content types and application-owned public rendering",
+ "A BTST database adapter",
+ "An image upload implementation when file fields are enabled",
+ "Authorization rules when content operations are protected",
+ ],
+ dependencies: ["A BTST database adapter", "Code-defined Zod content types"],
+ externalServices: [],
+ supportedFrameworks: MAINTAINED_FRAMEWORKS,
+ docsPath: "/plugins/cms",
+ demoPath: "https://www.better-stack.ai/playground?plugins=cms&view=preview",
+ sourcePath:
+ "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/cms",
+ },
"form-builder": {
topology: "Full-stack",
releaseStatus: "Preview",
@@ -102,6 +149,126 @@ export const PLUGIN_DECISIONS = {
sourcePath:
"https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/form-builder",
},
+ "ui-builder": {
+ topology: "Client-only",
+ relationship: "Dependent",
+ releaseStatus: "Preview",
+ supplies: [
+ "Visual page-list, creation, and editing routes",
+ "A component registry, layer editor, variables, and reusable block support",
+ "A PageRenderer for application-owned public routes",
+ "A CMS content-type declaration for storing page layers and status",
+ ],
+ adopterSupplies: [
+ "The components and blocks editors may place on a page",
+ "A public route that loads CMS page data and mounts PageRenderer",
+ "CMS authorization rules for page records",
+ "The application shell and deployment",
+ ],
+ dependencies: ["The CMS plugin, added automatically by the CLI"],
+ externalServices: [],
+ supportedFrameworks: MAINTAINED_FRAMEWORKS,
+ docsPath: "/plugins/ui-builder",
+ demoPath:
+ "https://www.better-stack.ai/playground?plugins=ui-builder&view=preview",
+ sourcePath:
+ "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/ui-builder",
+ },
+ kanban: {
+ topology: "Full-stack",
+ releaseStatus: "Preview",
+ supplies: [
+ "Board, column, task, and assignee data models with typed APIs and lifecycle hooks",
+ "SSR-aware board list, creation, and detail routes",
+ "Drag-and-drop column and task workflows with priority and assignee UI",
+ "Customizable hooks and ejectable Kanban pages",
+ ],
+ adopterSupplies: [
+ "A database adapter with isolated transaction support for persistent writes",
+ "Authorization rules plus user search and identity resolution when assignees are enabled",
+ "Product-specific workflow rules through configuration and lifecycle hooks",
+ "The application shell and deployment",
+ ],
+ dependencies: ["A database adapter with isolated transaction support"],
+ externalServices: [],
+ supportedFrameworks: MAINTAINED_FRAMEWORKS,
+ docsPath: "/plugins/kanban",
+ demoPath:
+ "https://www.better-stack.ai/playground?plugins=kanban&view=preview",
+ sourcePath:
+ "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/kanban",
+ },
+ comments: {
+ topology: "Full-stack",
+ releaseStatus: "Preview",
+ supplies: [
+ "Threaded comment and reaction data models with typed APIs and lifecycle hooks",
+ "Embeddable CommentThread and CommentCount components",
+ "A moderation route for pending, approved, and spam comments",
+ "Customizable hooks and ejectable moderation UI",
+ ],
+ adopterSupplies: [
+ "A BTST database adapter",
+ "The resource type and identifier that each thread belongs to",
+ "Authorization rules and authoritative request identity when access is protected",
+ "A user resolver when author names and avatars should be displayed",
+ ],
+ dependencies: ["A BTST database adapter", "An adopter-owned host resource"],
+ externalServices: [],
+ supportedFrameworks: MAINTAINED_FRAMEWORKS,
+ docsPath: "/plugins/comments",
+ sourcePath:
+ "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/comments",
+ },
+ media: {
+ topology: "Full-stack",
+ releaseStatus: "Preview",
+ supplies: [
+ "Asset and folder data models with typed upload, registration, and library APIs",
+ "An SSR-aware media-library route with search, folders, and asset actions",
+ "Embeddable MediaPicker and ImageInputField components",
+ "Local, S3-compatible, and Vercel Blob storage adapter implementations",
+ ],
+ adopterSupplies: [
+ "A database adapter with isolated transaction support for persistent writes",
+ "A configured storage adapter and its credentials or local upload directory",
+ "Allowed MIME types, size limits, URL prefixes, and authorization policy",
+ "The application routes and fields that embed the picker or image input",
+ ],
+ dependencies: [
+ "An isolating Prisma, Drizzle, or Kysely database adapter for persistent writes",
+ "A configured media storage adapter",
+ ],
+ externalServices: [
+ "Optional S3-compatible storage or Vercel Blob when the adopter selects those adapters",
+ ],
+ supportedFrameworks: MAINTAINED_FRAMEWORKS,
+ docsPath: "/plugins/media",
+ sourcePath:
+ "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/media",
+ },
+ "route-docs": {
+ topology: "Client-only",
+ releaseStatus: "Preview",
+ supplies: [
+ "A generated route-reference page for registered BTST client plugins",
+ "Route paths, path and query parameters, sitemap entries, and plugin context",
+ "Parameter-aware navigation to routes in the adopter's application",
+ "An ejectable Route Docs page over the packaged introspection runtime",
+ ],
+ adopterSupplies: [
+ "Registered BTST client plugins whose routes can be inspected",
+ "A deployment-level access boundary when route details should be private",
+ "Concrete parameter values before navigating to a dynamic route",
+ "The application shell and resolved site location",
+ ],
+ dependencies: ["Registered BTST client routes to inspect"],
+ externalServices: [],
+ supportedFrameworks: MAINTAINED_FRAMEWORKS,
+ docsPath: "/plugins/route-docs",
+ sourcePath:
+ "https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/route-docs",
+ },
"open-api": {
topology: "Backend-only",
releaseStatus: "Preview",
@@ -126,4 +293,31 @@ export const PLUGIN_DECISIONS = {
sourcePath:
"https://github.com/better-stack-ai/better-stack/tree/main/packages/stack/src/plugins/open-api",
},
-} as const satisfies Record