diff --git a/README.md b/README.md index a36baf71..ab02207b 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ function createAppClientStack(queryClient: QueryClient, origin: string) { ![The current Blog backend and client registrations point to the real published Blog route they produce.](docs/assets/product-proof/code-to-result.webp) -Blog adds publishing routes, API operations, its data model, hooks, SSR-aware pages, editor UI, SEO metadata, RSS, and sitemap entries. Other plugins have different boundaries: one-sided and companion plugins are labeled instead of being forced into a full-stack claim. +Blog adds publishing routes, API operations, its data model, hooks, SSR-aware pages, editor UI, published-page metadata, and sitemap entries. Other plugins have different boundaries: one-sided and companion plugins are labeled instead of being forced into a full-stack claim. ## Released capabilities @@ -121,7 +121,7 @@ Every capability below is installable from the released CLI. Follow its document | Capability | Topology | Outcome | | --- | --- | --- | -| [Blog](https://www.better-stack.ai/docs/plugins/blog) | Full-stack | Publishing workflow, routes, API, data model, editor, SEO, and RSS | +| [Blog](https://www.better-stack.ai/docs/plugins/blog) | Full-stack | Publishing workflow, routes, API, data model, editor, metadata, and sitemap entries | | [AI Chat](https://www.better-stack.ai/docs/plugins/ai-chat) | Full-stack | Streaming conversations, model integration, history, routes, and chat UI | | [CMS](https://www.better-stack.ai/docs/plugins/cms) | Full-stack | Typed content models, APIs, generated forms, and editorial UI | | [Form Builder](https://www.better-stack.ai/docs/plugins/form-builder) | Full-stack | Form authoring, validation, rendering, and submissions | diff --git a/docs/assets/product-proof/cms-proof.webp b/docs/assets/product-proof/cms-proof.webp new file mode 100644 index 00000000..601ae9ed Binary files /dev/null and b/docs/assets/product-proof/cms-proof.webp differ diff --git a/docs/assets/product-proof/comments-proof.webp b/docs/assets/product-proof/comments-proof.webp new file mode 100644 index 00000000..2810fc54 Binary files /dev/null and b/docs/assets/product-proof/comments-proof.webp differ diff --git a/docs/assets/product-proof/kanban-proof.webp b/docs/assets/product-proof/kanban-proof.webp new file mode 100644 index 00000000..828cc9cf Binary files /dev/null and b/docs/assets/product-proof/kanban-proof.webp differ diff --git a/docs/assets/product-proof/manifest.json b/docs/assets/product-proof/manifest.json index 02aa53ac..9ac48d08 100644 --- a/docs/assets/product-proof/manifest.json +++ b/docs/assets/product-proof/manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "assetRevision": 3, + "assetRevision": 4, "kitMaxBytes": 1800000, "forbiddenText": [ "Better Stack", @@ -124,6 +124,61 @@ "source": "../../../e2e/product-proof/capture.mjs", "decorative": false }, + { + "file": "cms-proof.webp", + "format": "webp", + "width": 1600, + "height": 900, + "maxBytes": 260000, + "alt": "Two real BTST CMS states showing code-defined content types in the dashboard and stored Product records in the generated application.", + "caption": "CMS turns application-defined Zod content types into an operator dashboard and stored records; public rendering remains an application-owned workflow.", + "source": "../../../e2e/product-proof/capture.mjs", + "decorative": false + }, + { + "file": "kanban-proof.webp", + "format": "webp", + "width": 1600, + "height": 900, + "maxBytes": 250000, + "alt": "Real BTST Kanban board with To Do, In Progress, and Done columns containing prioritized tasks.", + "caption": "The shipped Kanban workflow stores boards, columns, task order, priorities, and assignees in the adopter's database.", + "source": "../../../e2e/product-proof/capture.mjs", + "decorative": false + }, + { + "file": "comments-proof.webp", + "format": "webp", + "width": 1600, + "height": 900, + "maxBytes": 240000, + "alt": "Real BTST Comments moderation page showing an approved discussion attached to the shipping-plugin-catalog Blog resource.", + "caption": "A resource-bound comment passes through the real Comments API and appears in the supplied moderation workflow.", + "source": "../../../e2e/product-proof/capture.mjs", + "decorative": false + }, + { + "file": "media-proof.webp", + "format": "webp", + "width": 1600, + "height": 900, + "maxBytes": 240000, + "alt": "Real BTST Media library showing the checked-in product-proof image after upload, metadata registration, and search.", + "caption": "The proof fixture moves through the registered Media upload route into the searchable library, ready for picker and image-field reuse.", + "source": "../../../e2e/product-proof/capture.mjs", + "decorative": false + }, + { + "file": "route-docs-proof.webp", + "format": "webp", + "width": 1600, + "height": 900, + "maxBytes": 250000, + "alt": "Real BTST Route Docs page generated from the client stack, with registered plugin routes, paths, and parameter context.", + "caption": "Route Docs derives its reference from the application's registered client plugins rather than a separate hand-maintained route inventory.", + "source": "../../../e2e/product-proof/capture.mjs", + "decorative": false + }, { "file": "code-to-result.webp", "format": "webp", diff --git a/docs/assets/product-proof/media-proof.webp b/docs/assets/product-proof/media-proof.webp new file mode 100644 index 00000000..0664a537 Binary files /dev/null and b/docs/assets/product-proof/media-proof.webp differ diff --git a/docs/assets/product-proof/route-docs-proof.webp b/docs/assets/product-proof/route-docs-proof.webp new file mode 100644 index 00000000..92ffaa2a Binary files /dev/null and b/docs/assets/product-proof/route-docs-proof.webp differ diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx index 8366bdc6..866eeed9 100644 --- a/docs/content/docs/index.mdx +++ b/docs/content/docs/index.mdx @@ -66,7 +66,7 @@ runs. | Capability | Topology | What it adds | | --- | --- | --- | -| [Blog](/plugins/blog) | Full-stack | Publishing workflow, routes, API, data model, editor, SEO, and RSS | +| [Blog](/plugins/blog) | Full-stack | Publishing workflow, routes, API, data model, editor, metadata, and sitemap entries | | [AI Chat](/plugins/ai-chat) | Full-stack | Streaming conversations, model integration, history, routes, and UI | | [CMS](/plugins/cms) | Full-stack | Typed content models, APIs, generated forms, and editorial UI | | [Form Builder](/plugins/form-builder) | Full-stack | Form authoring, validation, rendering, and submissions | diff --git a/docs/content/docs/plugins/ai-chat.mdx b/docs/content/docs/plugins/ai-chat.mdx index e2f9b0de..dd3311f9 100644 --- a/docs/content/docs/plugins/ai-chat.mdx +++ b/docs/content/docs/plugins/ai-chat.mdx @@ -5,19 +5,9 @@ description: AI-powered chat functionality with conversation history, streaming, import { Tabs, Tab } from "fumadocs-ui/components/tabs"; import { Callout } from "fumadocs-ui/components/callout"; -import Image from "next/image"; +import { PluginEvaluatorHeader } from "@/components/plugin-evaluator"; -import aiChatProof from "../../../assets/product-proof/ai-chat-proof.webp"; - -
- - Real authenticated BTST AI Chat shell with an empty conversation-history sidebar, attachment control, and a product-context prompt ready to send. - -
- -This capture proves the shipped chat shell and prompt boundary. It deliberately does not fabricate a model response. - -[View interactive demo →](https://www.better-stack.ai/playground?plugins=ai-chat&view=preview) + ## Installation diff --git a/docs/content/docs/plugins/better-auth-ui.mdx b/docs/content/docs/plugins/better-auth-ui.mdx index 4b759286..5170996c 100644 --- a/docs/content/docs/plugins/better-auth-ui.mdx +++ b/docs/content/docs/plugins/better-auth-ui.mdx @@ -5,6 +5,9 @@ description: Add optional auth and account pages to an application that already import { Callout } from "fumadocs-ui/components/callout"; import { Tabs, Tab } from "fumadocs-ui/components/tabs"; +import { PluginEvaluatorHeader } from "@/components/plugin-evaluator"; + + [`@btst/better-auth-ui`](https://github.com/better-stack-ai/better-auth-ui) is the separately maintained Better Auth UI companion for BTST v3. It adds diff --git a/docs/content/docs/plugins/cms.mdx b/docs/content/docs/plugins/cms.mdx index 835d5eae..79145bd4 100644 --- a/docs/content/docs/plugins/cms.mdx +++ b/docs/content/docs/plugins/cms.mdx @@ -5,32 +5,9 @@ description: Headless CMS with code-defined content types, dynamic forms, and ag import { Tabs, Tab } from "fumadocs-ui/components/tabs"; import { Callout } from "fumadocs-ui/components/callout"; +import { PluginEvaluatorHeader } from "@/components/plugin-evaluator"; -import Image from "next/image"; - -The CMS plugin provides a headless content management system where developers define content types as Zod schemas in code. This "agency workflow" approach means: - -- **Developers** define the content model (schemas, validation rules, field descriptions) -- **Clients** manage content items through a friendly admin UI -- **TypeScript** provides end-to-end type safety when schema shapes change - -import cmsDemo from "../../../assets/cms-demo.png"; -import cmsDemo1 from "../../../assets/cms-demo-1.png"; -import cmsDemo2 from "../../../assets/cms-demo-2.png"; - -
- - CMS Plugin Demo - - - CMS Plugin Demo - - - CMS Plugin Demo - -
- -[View interactive demo →](https://www.better-stack.ai/playground?plugins=cms&view=preview) + ## Installation diff --git a/docs/content/docs/plugins/comments.mdx b/docs/content/docs/plugins/comments.mdx index dd5e2d09..af779c4d 100644 --- a/docs/content/docs/plugins/comments.mdx +++ b/docs/content/docs/plugins/comments.mdx @@ -5,17 +5,9 @@ description: Threaded comments with moderation, likes, replies, and embeddable C import { Tabs, Tab } from "fumadocs-ui/components/tabs"; import { Callout } from "fumadocs-ui/components/callout"; +import { PluginEvaluatorHeader } from "@/components/plugin-evaluator"; -The Comments plugin adds threaded commenting to any resource in your application — blog posts, Kanban tasks, CMS content, or your own custom pages. Comments are displayed with the embeddable `CommentThread` component and managed via a built-in moderation dashboard. - -**Key Features:** -- **Threaded replies** — Top-level comments and nested replies -- **Like system** — One like per user, optimistic UI updates, denormalized counter -- **Edit support** — Authors can edit their own comments; an "edited" timestamp is shown -- **Moderation dashboard** — Tabbed view (Pending / Approved / Spam) with bulk actions -- **Server-side user resolution** — `resolveUser` hook to embed author name and avatar in API responses -- **Optimistic updates** — New comments appear instantly with a "Pending approval" badge when `autoApprove: false` -- **Scroll-into-view lazy loading** — `CommentThread` is mounted only when it scrolls into the viewport + ## Installation diff --git a/docs/content/docs/plugins/index.mdx b/docs/content/docs/plugins/index.mdx index 4f08779e..2d974764 100644 --- a/docs/content/docs/plugins/index.mdx +++ b/docs/content/docs/plugins/index.mdx @@ -4,7 +4,7 @@ description: Available plugins and features for BTST --- import { Card, Cards } from "fumadocs-ui/components/card"; -import { Database, Hammer, Bot, Route, Layout, Columns3, MessageSquare, ImageIcon, ShieldCheck } from "lucide-react"; +import { Hammer } from "lucide-react"; import { PluginCatalogCard } from "@/components/plugin-evaluator"; @@ -30,55 +30,20 @@ workflow-heavy feature, and an intentionally one-sided plugin. ## More released plugins +
+ + + + + + + + +
+ +## Build a plugin + - } - description="AI-powered chat with conversation history, streaming, and customizable models." - /> - } - description="Headless CMS with custom content types, Zod schemas, and auto-generated forms." - /> - } - description="Visual drag-and-drop page builder with component registry and public rendering." - /> - } - description="Project management with boards, columns, tasks, drag-and-drop, and priority levels." - /> - } - description="Threaded comments with moderation, likes, replies, and embeddable comment threads." - /> - } - description="Media library with uploads, folders, picker UI, URL registration, and reusable image inputs." - /> - } - description="Auto-generated client route documentation with interactive navigation." - /> - } - description="Optional auth and account routes for applications that already run Better Auth." - /> - - Kanban Plugin Demo - Board View - - - -## Overview - -The Kanban plugin provides a full-featured project management system with: -- **Boards** - Organize different projects or workflows -- **Columns** - Define workflow stages (e.g., To Do, In Progress, Done) -- **Tasks** - Track individual work items with priorities -- **Assignees** - Assign users to tasks with avatar display -- **Drag-and-Drop** - Reorder tasks and columns with smooth animations -- **Priority Levels** - LOW, MEDIUM, HIGH, URGENT with visual badges -- **Organization Support** - Optional scoping by user or organization - -[View interactive demo →](https://www.better-stack.ai/playground?plugins=kanban&view=preview) + ## Installation diff --git a/docs/content/docs/plugins/media.mdx b/docs/content/docs/plugins/media.mdx index 23b507b7..be75a014 100644 --- a/docs/content/docs/plugins/media.mdx +++ b/docs/content/docs/plugins/media.mdx @@ -5,22 +5,9 @@ description: Media library, uploads, folders, picker UI, and reusable image inpu import { Tabs, Tab } from "fumadocs-ui/components/tabs"; import { Callout } from "fumadocs-ui/components/callout"; -import Image from "next/image"; +import { PluginEvaluatorHeader } from "@/components/plugin-evaluator"; -import mediaDemo1 from "../../../assets/media-demo-1.png"; - - - -The Media plugin gives you a built-in media library with folders, uploads, URL-based asset registration, and reusable picker components that can be embedded anywhere in your app. It works well as a standalone `/media` library route and as shared infrastructure for other plugins such as Blog, CMS, and Kanban. + ## Installation diff --git a/docs/content/docs/plugins/route-docs.mdx b/docs/content/docs/plugins/route-docs.mdx index a854cf0e..3d8b5a5f 100644 --- a/docs/content/docs/plugins/route-docs.mdx +++ b/docs/content/docs/plugins/route-docs.mdx @@ -5,26 +5,9 @@ description: Auto-generated client route documentation with interactive navigati import { Tabs, Tab } from "fumadocs-ui/components/tabs"; import { Callout } from "fumadocs-ui/components/callout"; -import Image from "next/image"; +import { PluginEvaluatorHeader } from "@/components/plugin-evaluator"; -import routeDocsDemo from "../../../assets/route-docs-demo.png"; - - - -The Route Docs plugin automatically generates documentation for all your client-side routes. It provides an interactive reference page that displays route paths, parameters, sitemap entries, and navigation tools. - -## Features - -- **Automatic Route Discovery** - Traverses all registered client plugins and extracts route metadata -- **Interactive Navigation** - Click any route to scroll to its details, or use the sidebar -- **Parameter Documentation** - Shows path and query parameters with types and descriptions -- **Sitemap Integration** - Displays sitemap entries for each route -- **Live Route Testing** - Fill in parameters and navigate directly to routes -- **Responsive Design** - Mobile-friendly with collapsible sidebar + ## Installation diff --git a/docs/content/docs/plugins/ui-builder.mdx b/docs/content/docs/plugins/ui-builder.mdx index 7e8bed80..90fb702d 100644 --- a/docs/content/docs/plugins/ui-builder.mdx +++ b/docs/content/docs/plugins/ui-builder.mdx @@ -5,39 +5,9 @@ description: Visual drag-and-drop page builder with component registry, variable import { Tabs, Tab } from "fumadocs-ui/components/tabs"; import { Callout } from "fumadocs-ui/components/callout"; -import { BookOpen } from "lucide-react"; -import Image from "next/image"; +import { PluginEvaluatorHeader } from "@/components/plugin-evaluator"; -import uiBuilderProof from "../../../assets/product-proof/ui-builder-proof.webp"; - - - -The UI Builder plugin provides a visual drag-and-drop page creation interface where administrators can create pages using pre-defined components. Pages are stored as JSON layers in the CMS and can be rendered on public routes. - - - - -For comprehensive documentation including interactive demos, block templates, and advanced customization options, visit [uibuilder.app](https://www.uibuilder.app/). - - -**Key Features:** -- **Visual Page Builder** - Drag-and-drop interface for creating pages with components -- **Component Registry** - Pre-built shadcn/ui components + custom component support -- **Variables** - Define page variables for dynamic content -- **CMS Integration** - Leverages the CMS plugin for data persistence -- **Public Page Rendering** - Render pages by slug with the `PageRenderer` component -- **Shared Authorization** - One CMS rule set gates browser UI and backend operations - -[View interactive demo →](https://www.better-stack.ai/playground?plugins=ui-builder&view=preview) + ## Installation diff --git a/docs/src/components/plugin-evaluator.tsx b/docs/src/components/plugin-evaluator.tsx index a74444e5..8dceec3e 100644 --- a/docs/src/components/plugin-evaluator.tsx +++ b/docs/src/components/plugin-evaluator.tsx @@ -2,17 +2,30 @@ import { ArrowRight, ExternalLink } from "lucide-react"; import Image, { type StaticImageData } from "next/image"; import Link from "next/link"; import type { ReactNode } from "react"; +import aiChatProof from "../../assets/product-proof/ai-chat-proof.webp"; import blogProof from "../../assets/product-proof/blog-proof.webp"; +import cmsProof from "../../assets/product-proof/cms-proof.webp"; +import commentsProof from "../../assets/product-proof/comments-proof.webp"; import formBuilderProof from "../../assets/product-proof/form-builder-proof.webp"; -import productProofManifest from "../../assets/product-proof/manifest.json"; +import kanbanProof from "../../assets/product-proof/kanban-proof.webp"; +import mediaProof from "../../assets/product-proof/media-proof.webp"; import openApiProof from "../../assets/product-proof/openapi-proof.webp"; +import productProofManifest from "../../assets/product-proof/manifest.json"; +import routeDocsProof from "../../assets/product-proof/route-docs-proof.webp"; +import uiBuilderProof from "../../assets/product-proof/ui-builder-proof.webp"; import { PLUGINS } from "@btst/codegen/meta"; -type RepresentativePluginKey = "blog" | "form-builder" | "open-api"; +type EvaluatorPluginKey = (typeof PLUGINS)[number]["key"]; type PluginDecisionMeta = NonNullable< (typeof PLUGINS)[number]["decision"] >; -type DecisionActionTarget = "demo" | "docs" | "installation" | "workflow"; +type DecisionActionTarget = + | "demo" + | "docs" + | "installation" + | "setup" + | "source" + | "workflow"; interface WorkflowStepCopy { label: string; @@ -29,7 +42,8 @@ interface ResolvedDecisionAction extends DecisionActionCopy { external: boolean; } -interface RepresentativeCopy { +interface EvaluatorCopy { + catalogTitle?: string; outcome: string; audience: string; ownership: string; @@ -38,7 +52,7 @@ interface RepresentativeCopy { actions: readonly [DecisionActionCopy, DecisionActionCopy]; } -const representativeCopy = { +const evaluatorCopy = { blog: { outcome: "Publish and manage a content section inside the React application you already run.", @@ -72,6 +86,74 @@ const representativeCopy = { { label: "Install Blog", target: "installation" }, ], }, + "ai-chat": { + outcome: + "Add a streaming conversation surface while choosing the model, access policy, and operating limits yourself.", + audience: + "React teams that already know which AI SDK model provider they want and need chat history, tools, and product UI inside their app.", + ownership: + "You select and pay the model provider. Authenticated history stays in your database, and ejected chat pages become editable application code while streaming and data behavior remain packaged.", + workflow: [ + { + label: "Choose a model", + detail: + "Pass an AI SDK model and keep its credentials in your server environment.", + }, + { + label: "Set access", + detail: + "Use authenticated persistence with typed rules or choose explicit stateless public mode.", + }, + { + label: "Stream", + detail: + "Run prompts, tools, and optional attachments through the supplied chat route.", + }, + { + label: "Keep context", + detail: + "Store identity-scoped conversations in your database when authenticated mode is enabled.", + }, + ], + catalogAction: { label: "Explore AI Chat", target: "docs" }, + actions: [ + { label: "View chat shell", target: "demo" }, + { label: "Install AI Chat", target: "installation" }, + ], + }, + cms: { + outcome: + "Define structured content in TypeScript and give operators generated forms for managing it.", + audience: + "Teams that want developers to own content models while editors manage records through an in-app admin interface.", + ownership: + "Content models live in your code and records stay in your database. You own public rendering and uploads; the packaged admin pages can be overridden or ejected.", + workflow: [ + { + label: "Model", + detail: "Define each content type as a shared Zod schema in your app.", + }, + { + label: "Generate forms", + detail: + "Turn schema fields and metadata into validated operator-facing forms.", + }, + { + label: "Manage", + detail: "Create and edit typed content records through the admin routes.", + }, + { + label: "Render", + detail: + "Load records with packaged hooks and present them on application-owned routes.", + }, + ], + catalogAction: { label: "Explore CMS", target: "docs" }, + actions: [ + { label: "View CMS workflow", target: "demo" }, + { label: "Install CMS", target: "installation" }, + ], + }, "form-builder": { outcome: "Let operators design forms visually, publish them inside your app, and collect validated submissions.", @@ -105,6 +187,176 @@ const representativeCopy = { { label: "Install Form Builder", target: "installation" }, ], }, + "ui-builder": { + catalogTitle: "UI Builder", + outcome: + "Let operators compose pages visually, store the layout through CMS, and render it on routes you own.", + audience: + "Teams that already use the BTST CMS plugin and want a constrained visual editor over application-approved components.", + ownership: + "The CMS-backed page record stays in your database. You choose the component registry and public route; editor views can be ejected while CMS data behavior remains packaged.", + workflow: [ + { + label: "Add CMS", + detail: + "Use the required CMS plugin; the CLI adds it automatically when selected.", + }, + { + label: "Register components", + detail: + "Define the components and blocks an operator is allowed to place.", + }, + { + label: "Compose", + detail: "Arrange layers, variables, and properties in the visual editor.", + }, + { + label: "Render", + detail: + "Load the CMS record and mount PageRenderer on an application-owned public route.", + }, + ], + catalogAction: { label: "Explore UI Builder", target: "docs" }, + actions: [ + { label: "View builder workflow", target: "demo" }, + { label: "Install UI Builder", target: "installation" }, + ], + }, + kanban: { + outcome: + "Add boards, columns, tasks, priorities, and drag-and-drop workflows inside your product.", + audience: + "Product teams that need an application-native work board and want to connect it to their own users and authorization rules.", + ownership: + "Boards and tasks stay in your database. Your app supplies identity and workflow policy; packaged Kanban pages can be customized or ejected.", + workflow: [ + { + label: "Create boards", + detail: "Use supplied routes and APIs to create a board and its columns.", + }, + { + label: "Add work", + detail: "Create prioritized tasks and connect assignees through your user resolver.", + }, + { + label: "Move", + detail: + "Reorder columns and drag tasks through isolated database transactions.", + }, + { + label: "Enforce policy", + detail: + "Apply app-owned authorization rules and domain hooks to every operation.", + }, + ], + catalogAction: { label: "Explore Kanban", target: "docs" }, + actions: [ + { label: "View Kanban board", target: "demo" }, + { label: "Install Kanban", target: "installation" }, + ], + }, + comments: { + outcome: + "Attach threaded discussion and moderation to a resource your application already owns.", + audience: + "Teams that need comments on posts, tasks, content records, or a custom resource without adopting a hosted discussion service.", + ownership: + "Comments and reactions stay in your database. You define resource identity, access rules, and author resolution; the moderation page can be ejected.", + workflow: [ + { + label: "Name the resource", + detail: + "Mount CommentThread with the resource type and identifier owned by your app.", + }, + { + label: "Resolve identity", + detail: + "Connect request identity, typed authorization rules, and optional author profiles.", + }, + { + label: "Discuss", + detail: "Create replies and reactions through the supplied typed APIs and UI.", + }, + { + label: "Moderate", + detail: + "Review pending, approved, and spam comments on the built-in route.", + }, + ], + catalogAction: { label: "Explore Comments", target: "docs" }, + actions: [ + { label: "Understand the workflow", target: "workflow" }, + { label: "Install Comments", target: "installation" }, + ], + }, + media: { + outcome: + "Upload, organize, register, and reuse media through a library that runs with your storage.", + audience: + "Teams that need a shared media layer for product features while keeping files, metadata, and storage credentials under their control.", + ownership: + "Asset metadata stays in your database and files stay in the storage you configure. You own limits, access policy, and embedding; the library view can be ejected.", + workflow: [ + { + label: "Choose storage", + detail: + "Configure local, S3-compatible, or Vercel Blob storage in your backend.", + }, + { + label: "Upload or register", + detail: + "Send a file through the matching protocol or register an allowed asset URL.", + }, + { + label: "Organize", + detail: "Search assets, maintain folders, and edit metadata in the library.", + }, + { + label: "Reuse", + detail: + "Embed MediaPicker or ImageInputField wherever your application needs an asset.", + }, + ], + catalogAction: { label: "Explore Media", target: "docs" }, + actions: [ + { label: "Understand the workflow", target: "workflow" }, + { label: "Install Media", target: "installation" }, + ], + }, + "route-docs": { + outcome: + "Generate an explorable reference for the client routes already registered in your BTST stack.", + audience: + "Teams that want route paths, parameters, sitemap entries, and navigation context visible inside a development or protected application surface.", + ownership: + "The reference is derived from your registered client stack and served by your app. You control which routes exist, who can see the page, and whether to eject its UI.", + workflow: [ + { + label: "Register routes", + detail: "Compose the client plugins whose route definitions should appear.", + }, + { + label: "Inspect", + detail: + "Read route paths, typed parameters, metadata, and sitemap declarations.", + }, + { + label: "Generate", + detail: + "Render the route inventory with its owning plugin and dynamic context.", + }, + { + label: "Navigate", + detail: + "Supply concrete parameters and open a resolved route in your application.", + }, + ], + catalogAction: { label: "Explore Route Docs", target: "docs" }, + actions: [ + { label: "Understand the output", target: "workflow" }, + { label: "Install Route Docs", target: "installation" }, + ], + }, "open-api": { outcome: "Expose an OpenAPI 3.1 contract for registered BTST routes and, when useful, an interactive API reference.", @@ -137,30 +389,73 @@ const representativeCopy = { { label: "Install OpenAPI", target: "installation" }, ], }, -} as const satisfies Record; + "better-auth-ui": { + catalogTitle: "Better Auth UI", + outcome: + "Add auth and account routes to a BTST client stack that already has a Better Auth backend.", + audience: + "Teams already operating Better Auth that want its auth and account UI composed into the maintained BTST framework paths.", + ownership: + "Your application keeps the Better Auth server, database, providers, secrets, sessions, and deployment. The companion supplies client routes and UI; it does not create or host authentication.", + workflow: [ + { + label: "Operate Better Auth", + detail: + "Keep the existing server endpoint, schema, providers, secrets, and browser client.", + }, + { + label: "Add the companion", + detail: "Register auth and account client plugins in the BTST client stack.", + }, + { + label: "Mount routes", + detail: + "Serve sign-in, recovery, account, security, and settings views under your site path.", + }, + { + label: "Refresh sessions", + detail: + "Use the generated framework-native refresh seam after session changes.", + }, + ], + catalogAction: { label: "Explore Better Auth UI", target: "docs" }, + actions: [ + { label: "View companion source", target: "source" }, + { label: "Install the companion", target: "setup" }, + ], + }, +} as const satisfies Record; -const representativeAssets = { +const evaluatorAssets: Partial< + Record +> = { blog: { file: "blog-proof.webp", image: blogProof }, + "ai-chat": { file: "ai-chat-proof.webp", image: aiChatProof }, + cms: { file: "cms-proof.webp", image: cmsProof }, "form-builder": { file: "form-builder-proof.webp", image: formBuilderProof, }, + "ui-builder": { file: "ui-builder-proof.webp", image: uiBuilderProof }, + kanban: { file: "kanban-proof.webp", image: kanbanProof }, + comments: { file: "comments-proof.webp", image: commentsProof }, + media: { file: "media-proof.webp", image: mediaProof }, + "route-docs": { file: "route-docs-proof.webp", image: routeDocsProof }, "open-api": { file: "openapi-proof.webp", image: openApiProof }, -} as const satisfies Record< - RepresentativePluginKey, - { file: string; image: StaticImageData } ->; +}; -function getRepresentativePlugin(key: RepresentativePluginKey) { +function getEvaluatorPlugin(key: EvaluatorPluginKey) { const plugin = PLUGINS.find((candidate) => candidate.key === key); if (!plugin?.decision) { throw new Error(`Missing evaluator metadata for ${key}`); } - return { plugin, decision: plugin.decision, copy: representativeCopy[key] }; + const copy: EvaluatorCopy = evaluatorCopy[key]; + return { plugin, decision: plugin.decision, copy }; } -function getProofAsset(key: RepresentativePluginKey) { - const asset = representativeAssets[key]; +function getProofAsset(key: EvaluatorPluginKey) { + const asset = evaluatorAssets[key]; + if (!asset) return null; const copy = productProofManifest.assets.find( (candidate) => candidate.file === asset.file, ); @@ -289,7 +584,7 @@ function WorkflowStrip({ } function resolveAction( - key: RepresentativePluginKey, + key: EvaluatorPluginKey, decision: PluginDecisionMeta, action: DecisionActionCopy, ): ResolvedDecisionAction { @@ -303,6 +598,14 @@ function resolveAction( return { ...action, href: decision.docsPath, external: false }; case "installation": return { ...action, href: "#installation", external: false }; + case "setup": + return { + ...action, + href: "#generate-the-minimal-integration", + external: false, + }; + case "source": + return { ...action, href: decision.sourcePath, external: true }; case "workflow": return { ...action, href: `#${key}-workflow`, external: false }; } @@ -311,9 +614,9 @@ function resolveAction( export function PluginEvaluatorHeader({ pluginKey, }: { - pluginKey: RepresentativePluginKey; + pluginKey: EvaluatorPluginKey; }) { - const { decision, copy } = getRepresentativePlugin(pluginKey); + const { decision, copy } = getEvaluatorPlugin(pluginKey); const proof = getProofAsset(pluginKey); const actions = copy.actions.map((action) => resolveAction(pluginKey, decision, action), @@ -324,7 +627,13 @@ export function PluginEvaluatorHeader({ aria-labelledby={`${pluginKey}-evaluator-title`} className="not-prose my-8 overflow-hidden rounded-2xl border border-fd-border bg-fd-background shadow-sm" > -
+

@@ -340,7 +649,7 @@ export function PluginEvaluatorHeader({ {copy.outcome}

- {actions.map((action, index) => ( + {actions.map((action, index) => (
-
- {proof.alt} -
- {proof.caption} -
-
+ {proof ? ( +
+ {proof.alt} +
+ {proof.caption} +
+
+ ) : null}
@@ -404,10 +715,7 @@ export function PluginEvaluatorHeader({
- + ); } @@ -415,9 +723,9 @@ export function PluginEvaluatorHeader({ export function PluginCatalogCard({ pluginKey, }: { - pluginKey: RepresentativePluginKey; + pluginKey: EvaluatorPluginKey; }) { - const { plugin, decision, copy } = getRepresentativePlugin(pluginKey); + const { plugin, decision, copy } = getEvaluatorPlugin(pluginKey); const proof = getProofAsset(pluginKey); const catalogAction = resolveAction( pluginKey, @@ -427,13 +735,15 @@ export function PluginCatalogCard({ return (
- {proof.alt} + {proof ? ( + {proof.alt} + ) : null}

@@ -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; + "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: MAINTAINED_FRAMEWORKS, + docsPath: "/plugins/better-auth-ui", + sourcePath: "https://github.com/better-stack-ai/better-auth-ui", + }, +} as const satisfies Record;