@btst/stack 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +115 -223
  2. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +2 -2
  3. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +2 -2
  4. package/dist/packages/stack/src/plugins/blog/client/components/shared/format-post-date.cjs +13 -0
  5. package/dist/packages/stack/src/plugins/blog/client/components/shared/format-post-date.mjs +11 -0
  6. package/dist/packages/stack/src/plugins/blog/client/components/shared/post-card.cjs +4 -6
  7. package/dist/packages/stack/src/plugins/blog/client/components/shared/post-card.mjs +4 -6
  8. package/dist/plugins/blog/api/index.d.cts +2 -2
  9. package/dist/plugins/blog/api/index.d.mts +2 -2
  10. package/dist/plugins/blog/api/index.d.ts +2 -2
  11. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  12. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  13. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  14. package/dist/plugins/blog/client/index.d.cts +2 -2
  15. package/dist/plugins/blog/client/index.d.mts +2 -2
  16. package/dist/plugins/blog/client/index.d.ts +2 -2
  17. package/dist/plugins/blog/query-keys.d.cts +2 -2
  18. package/dist/plugins/blog/query-keys.d.mts +2 -2
  19. package/dist/plugins/blog/query-keys.d.ts +2 -2
  20. package/dist/plugins/kanban/api/index.d.cts +1 -1
  21. package/dist/plugins/kanban/api/index.d.mts +1 -1
  22. package/dist/plugins/kanban/api/index.d.ts +1 -1
  23. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  24. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  25. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  26. package/dist/plugins/kanban/client/index.d.cts +1 -1
  27. package/dist/plugins/kanban/client/index.d.mts +1 -1
  28. package/dist/plugins/kanban/client/index.d.ts +1 -1
  29. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  30. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  31. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  32. package/dist/shared/{stack.D2y4gl03.d.mts → stack.94kN3pEK.d.mts} +5 -5
  33. package/dist/shared/{stack.rmWRtSjs.d.cts → stack.BTStHVTy.d.cts} +5 -5
  34. package/dist/shared/{stack.CZ1h_T4k.d.mts → stack.BggTHKlo.d.mts} +90 -90
  35. package/dist/shared/{stack.DiYg7Jw1.d.cts → stack.CtrnoQgH.d.cts} +90 -90
  36. package/dist/shared/{stack.CnDMBj4z.d.ts → stack.DfrRYOIl.d.ts} +5 -5
  37. package/dist/shared/{stack.DQpyA3Cq.d.ts → stack.lPKwd_v0.d.ts} +90 -90
  38. package/package.json +1 -1
  39. package/src/__tests__/initial-identity-hydration.test.tsx +72 -1
  40. package/src/plugins/blog/__tests__/date-hydration.test.tsx +78 -0
  41. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +2 -2
  42. package/src/plugins/blog/client/components/shared/format-post-date.ts +12 -0
  43. package/src/plugins/blog/client/components/shared/post-card.tsx +4 -6
  44. package/dist/shared/{stack.Y91icYuv.d.ts → stack.0y6iTP03.d.ts} +75 -75
  45. package/dist/shared/{stack.CMc8mLyD.d.mts → stack.B1UrNoVl.d.mts} +75 -75
  46. package/dist/shared/{stack.DtZgEX84.d.cts → stack.CKc_Bais.d.cts} +6 -6
  47. package/dist/shared/{stack.DtZgEX84.d.mts → stack.CKc_Bais.d.mts} +6 -6
  48. package/dist/shared/{stack.DtZgEX84.d.ts → stack.CKc_Bais.d.ts} +6 -6
  49. package/dist/shared/{stack._CObfJbw.d.cts → stack.Mak7pzIV.d.cts} +75 -75
package/README.md CHANGED
@@ -1,289 +1,181 @@
1
- # @btst/stack — BTST
1
+ # BTST
2
2
 
3
3
  <div align="center">
4
4
 
5
- **Installable full-stack features for React apps**
6
- Framework-agnostic. Database-flexible. No lock-in.
5
+ ## Add complete features to the React app you already own.
7
6
 
8
- [![npm](https://img.shields.io/npm/v/@btst/stack.svg)](https://www.npmjs.com/package/@btst/stack)
9
- [![MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
-
11
- [Docs](https://www.better-stack.ai/docs) · [Examples](./examples) · [Issues](https://github.com/better-stack-ai/better-stack/issues)
12
-
13
- </div>
7
+ BTST is an open-source TypeScript system for installing full-stack features into existing React applications.
14
8
 
15
- ---
9
+ It is built for React/TypeScript developers and small product teams adding a substantial feature to an app they already have. A full-stack plugin can bring the routes, APIs, database schema, hooks, SSR-aware pages, and customizable UI that its feature needs.
16
10
 
17
- ## What is BTST?
11
+ **You own the whole application.** Your app, data, deployment, and ejected UI stay yours. BTST runs inside your stack as an open-source dependency you can inspect, fork, or replace—never as a required hosted control plane.
18
12
 
19
- BTST lets you **install production-ready app features as npm packages**.
13
+ [**Quickstart with Blog**](#quickstart-blog-in-an-existing-nextjs-app) · [View the live Blog](https://www.better-stack.ai/p/blog) · [Read the docs](https://www.better-stack.ai/docs)
20
14
 
21
- Instead of spending weeks building the same things again and again
22
- (routes, APIs, database schemas, SSR, SEO, forms…):
15
+ [![npm](https://img.shields.io/npm/v/@btst/stack.svg)](https://www.npmjs.com/package/@btst/stack)
16
+ [![MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](./LICENSE.md)
23
17
 
24
- ```bash
25
- npm install @btst/stack
26
- ````
18
+ ![BTST promise above a focused view of the real Blog route from the generated application.](docs/assets/product-proof/readme-hero.webp)
27
19
 
28
- Enable the features you need and keep building your product.
20
+ <sub>Real output from the repository's generated Next.js application.</sub>
29
21
 
30
- ### Available plugins
22
+ </div>
31
23
 
32
- | Plugin | Description |
33
- |--------|-------------|
34
- | **Blog** | Content management, editor, drafts, publishing, SEO, RSS feeds |
35
- | **AI Chat** | AI-powered chat with conversation history, streaming, and customizable models |
36
- | **CMS** | Headless CMS with custom content types, Zod schemas, and auto-generated forms |
37
- | **Form Builder** | Dynamic form builder with drag-and-drop editor, submissions, and validation |
38
- | **UI Builder** | Visual drag-and-drop page builder with component registry and public rendering |
39
- | **Kanban** | Project management with boards, columns, tasks, drag-and-drop, and priority levels |
40
- | **Media** | Media library with uploads, folders, picker UI, URL registration, and reusable image inputs |
41
- | **OpenAPI** | Auto-generated API documentation with interactive Scalar UI |
42
- | **Route Docs** | Auto-generated client route documentation with interactive navigation |
43
- | **Comments** | Commenting system with moderation, likes, and nested replies |
24
+ ## Where BTST fits
44
25
 
45
- Full-stack plugins ship separate frontend and backend definitions: routes,
46
- APIs, database models, React components, SSR, and SEO—already wired. Intentional
47
- one-sided plugins stay one-sided: OpenAPI is backend-only, Route Docs is
48
- client-only, and UI Builder is client-only over CMS.
26
+ BTST is **more complete than a UI kit, more incremental than a starter application, and more ownable than a hosted feature service**.
49
27
 
50
- **Want a specific plugin?** [Open an issue](https://github.com/better-stack-ai/better-stack/issues/new) and let us know!
28
+ | Starting point | What it gives you | Adoption boundary |
29
+ | --- | --- | --- |
30
+ | UI kit | Components and interaction primitives | Your team still builds the feature's routes, data model, APIs, and workflows |
31
+ | Starter application | An application foundation with initial features | You adopt its application structure as the place you start |
32
+ | Hosted feature service | A feature operated behind a vendor service boundary | Its control plane, runtime, or data path remains outside your application |
33
+ | **BTST** | An installable feature runtime, generated integration, and customizable views | Add one capability inside the app, database, and deployment you already operate |
51
34
 
52
- ---
35
+ Start with one plugin and add more only when they are useful. BTST does not replace your application foundation or require a hosted control plane.
53
36
 
54
- ## Why use it?
37
+ ![Diagram separating the application and operating boundaries a team owns from the BTST runtimes and plugins running inside it.](docs/assets/product-proof/ownership-diagram.svg)
55
38
 
56
- * **Installable features** – real product features, not just UI
57
- * **Framework-agnostic** – Next.js, React Router, TanStack Router, Remix
58
- * **Database-flexible** – Prisma, Drizzle, Kysely, MongoDB
59
- * **Zero boilerplate** – no manual route or API wiring
60
- * **Type-safe** – end-to-end TypeScript
39
+ <p align="center"><sub>BTST composes client and backend plugins inside the application; the app shell, ejected views, database, deployment, and external services remain yours.</sub></p>
61
40
 
62
- You keep your codebase, database, and deployment.
41
+ ## Quickstart: Blog in an existing Next.js app
63
42
 
64
- ---
43
+ This local evaluation uses the public stable releases with Node.js 22, an existing Next.js 15+ App Router application, and Radix-based shadcn/ui with CSS variables. If shadcn/ui is not configured yet, initialize that verified base and add the components Blog needs:
65
44
 
66
- ## Minimal setup (Next.js)
45
+ ```bash
46
+ pnpm dlx shadcn@4.19.1 init --base radix --preset nova --css-variables
47
+ pnpm dlx shadcn@4.19.1 add button dropdown-menu sonner --yes
48
+ ```
67
49
 
68
- ```ts title="lib/stack.ts"
69
- import { createBackendStack } from "@btst/stack/api"
70
- import { blogBackendPlugin } from "@btst/stack/plugins/blog/api"
71
- import { createMemoryAdapter } from "@btst/adapter-memory"
50
+ Render the generated `<Toaster />` in your root layout and start from a clean commit you can review. The [complete prerequisite checklist](https://www.better-stack.ai/docs/installation#quickstart-prerequisites) covers the remaining application requirements.
72
51
 
73
- function createAppStack() {
74
- return createBackendStack({
75
- basePath: "/api/data",
76
- plugins: {
77
- blog: blogBackendPlugin()
78
- },
79
- adapter: (db) => createMemoryAdapter(db)({})
80
- })
81
- }
52
+ Run the released generator from the application root:
82
53
 
83
- type AppStack = ReturnType<typeof createAppStack>
84
- const globalForStack = globalThis as typeof globalThis & {
85
- __btst_stack__?: AppStack
86
- }
87
- export const myStack = globalForStack.__btst_stack__ ??= createAppStack()
88
- export const { handler, dbSchema } = myStack
54
+ ```bash
55
+ pnpm dlx @btst/codegen@0.2.0 init \
56
+ --framework nextjs \
57
+ --adapter memory \
58
+ --plugins blog
89
59
  ```
90
60
 
91
- ```tsx title="lib/stack-client.tsx"
92
- import {
93
- createClientStack,
94
- type ClientPluginEndpointOverride,
95
- } from "@btst/stack/client"
96
- import { blogClientPlugin } from "@btst/stack/plugins/blog/client"
97
- import type { QueryClient } from "@tanstack/react-query"
61
+ The generator shows detected-file and conflict prompts before it writes. It installs `@btst/stack@3.0.0`, registers Blog on the backend and client, mounts the API and page routes, adds the plugin CSS, and wires the shared provider.
98
62
 
99
- export interface StackClientOptions {
100
- apiOrigin: string
101
- siteOrigin: string
102
- }
63
+ Set the trusted local origins in `.env.local`:
103
64
 
104
- export function createAppClientStack(
105
- queryClient: QueryClient,
106
- options: StackClientOptions & { headers?: HeadersInit },
107
- ) {
108
- const { apiOrigin, siteOrigin } = options
109
- const crossOriginBlogEndpoint = apiOrigin === siteOrigin
110
- ? undefined
111
- : {
112
- api: {
113
- baseURL: apiOrigin,
114
- basePath: "/api/data",
115
- credentials: "include",
116
- },
117
- } satisfies ClientPluginEndpointOverride
65
+ ```dotenv
66
+ BTST_SITE_URL=http://localhost:3000
67
+ BTST_API_URL=http://localhost:3000
68
+ ```
118
69
 
119
- return createClientStack({
120
- api: {
121
- baseURL: apiOrigin,
122
- basePath: "/api/data",
123
- ...(options.headers ? { headers: options.headers } : {}),
124
- },
125
- site: { baseURL: siteOrigin, basePath: "/pages" },
126
- queryClient,
127
- plugins: {
128
- blog: blogClientPlugin()
129
- },
130
- ...(crossOriginBlogEndpoint
131
- ? { endpoints: { blog: crossOriginBlogEndpoint } }
132
- : {}),
133
- })
134
- }
70
+ Then start the app and open the generated route:
135
71
 
136
- export function getStackClient(
137
- queryClient: QueryClient,
138
- options: StackClientOptions,
139
- ) {
140
- return createAppClientStack(queryClient, options)
141
- }
72
+ ```bash
73
+ pnpm dev
74
+ # http://localhost:3000/pages/blog
142
75
  ```
143
76
 
144
- The generated `lib/stack-client.server.ts` resolves these origins from trusted
145
- deployment configuration (`BTST_API_URL` and `BTST_SITE_URL`), defaults the API
146
- to the trusted site origin, and forwards filtered credentials only to that API.
147
- It fails closed in production if no trusted origin is available.
148
- Existing same-origin Next.js installs may keep `NEXT_PUBLIC_BASE_URL` while
149
- migrating; new deployments should prefer the separate site/API variables.
77
+ `/pages/blog` is the success condition. A fresh local route can be empty until you create content. The memory adapter resets with the process and is for evaluation and tests, not production persistence; Blog image uploads also remain an explicit application override. Continue with the [production and manual setup reference](https://www.better-stack.ai/docs/installation) before deploying.
150
78
 
151
- Use the v3 framework entry factories for the two catch-all routes:
79
+ ![Real BTST Blog page with three published product-update posts in the generated Next.js application.](docs/assets/product-proof/blog-proof.webp)
152
80
 
153
- ```ts title="app/api/data/[[...all]]/route.ts"
154
- import { toNextRouteHandlers } from "@btst/stack/next"
155
- import { handler } from "@/lib/stack"
81
+ <p align="center"><sub>The same generated Blog route after deterministic sample content is added.</sub></p>
156
82
 
157
- export const { GET, POST, PUT, PATCH, DELETE } =
158
- toNextRouteHandlers(handler)
159
- ```
83
+ ### The registration that produces the route
160
84
 
161
- ```tsx title="app/(request)/pages/[[...all]]/page.tsx"
162
- import { createNextPage } from "@btst/stack/next"
163
- import { headers } from "next/headers"
164
- import { getStackClientForRequest } from "@/lib/stack-client.server"
165
- import { getOrCreateQueryClient } from "@/lib/query-client"
85
+ The stable-v3 seam is deliberately small: the backend plugin supplies Blog's server behavior and data model; the client plugin supplies its routes and UI. The generator provides the surrounding adapter, API/site runtime, QueryClient, catch-all routes, and provider wiring.
166
86
 
167
- export const dynamic = "force-dynamic"
87
+ ```ts
88
+ import { createBackendStack } from "@btst/stack/api"
89
+ import { createMemoryAdapter } from "@btst/adapter-memory"
90
+ import { blogBackendPlugin } from "@btst/stack/plugins/blog/api"
168
91
 
169
- const page = createNextPage({
170
- getStackClient: async (queryClient) =>
171
- getStackClientForRequest(queryClient, {
172
- headers: new Headers(await headers()),
173
- }),
174
- getQueryClient: getOrCreateQueryClient,
92
+ createBackendStack({
93
+ basePath: "/api/data",
94
+ adapter: (db) => createMemoryAdapter(db)({}),
95
+ plugins: { blog: blogBackendPlugin() },
175
96
  })
176
- export default page.Page
177
- export const generateMetadata = page.generateMetadata
178
97
  ```
179
98
 
180
- The request layout at `app/(request)/pages/layout.tsx` hydrates trusted client
181
- origins into the shared provider in `app/pages/client-layout.tsx`. Put SSG/ISR
182
- routes under `app/(static)/pages` with a header-free layout; route groups do not
183
- change the public `/pages/*` URLs.
184
-
185
- Wrap the pages subtree with one `StackProvider`:
186
-
187
99
  ```tsx
188
- // app/pages/client-layout.tsx
189
- "use client"
190
-
191
- function PagesClientLayout({ children, clientOrigins }: {
192
- children: React.ReactNode
193
- clientOrigins: StackClientOptions
194
- }) {
195
- const queryClient = getOrCreateQueryClient()
196
- const clientStack = useMemo(
197
- () => getStackClient(queryClient, clientOrigins),
198
- [clientOrigins.apiOrigin, clientOrigins.siteOrigin, queryClient],
199
- )
200
-
201
- return (
202
- <QueryClientProvider client={queryClient}>
203
- <StackProvider
204
- stack={clientStack}
205
- router={nextRouter()}
206
- auth={authProvider}
207
- overrides={{ blog: { uploadImage } }}
208
- >
209
- {children}
210
- </StackProvider>
211
- </QueryClientProvider>
212
- )
213
- }
214
- ```
215
-
216
- API, site, and QueryClient runtime belong on the resolved client stack; router
217
- and auth services belong on the provider. Plugin overrides contain only
218
- plugin-specific customization. See the [full installation guide](https://www.better-stack.ai/docs/installation)
219
- for QueryClient wiring, database adapters, all three frameworks, and auth.
220
-
221
- ## Database schemas & migrations
222
-
223
- Generate schemas and run migrations through the v3 codegen CLI. It runs the
224
- aligned Better DB CLI in isolation, so its dependencies and `btst` binary do
225
- not enter your application graph:
100
+ import { createClientStack } from "@btst/stack/client"
101
+ import { blogClientPlugin } from "@btst/stack/plugins/blog/client"
102
+ import type { QueryClient } from "@tanstack/react-query"
226
103
 
227
- ```bash
228
- npx @btst/codegen@0.2.0 generate --orm drizzle --config lib/stack.ts --output db/schema.ts
104
+ function createAppClientStack(queryClient: QueryClient, origin: string) {
105
+ return createClientStack({
106
+ api: { baseURL: origin, basePath: "/api/data" },
107
+ site: { baseURL: origin, basePath: "/pages" },
108
+ queryClient,
109
+ plugins: { blog: blogClientPlugin() },
110
+ })
111
+ }
229
112
  ```
230
113
 
231
- Supports Prisma, Drizzle, MongoDB and Kysely SQL dialects.
114
+ ![The current Blog backend and client registrations point to the real published Blog route they produce.](docs/assets/product-proof/code-to-result.webp)
232
115
 
233
- ---
116
+ 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.
234
117
 
235
- ## Shadcn Registry
118
+ ## Released capabilities
236
119
 
237
- Each plugin's UI layer is available as a [shadcn registry](https://ui.shadcn.com/docs/registry) block. Use it to **eject and fully customize** the page components while keeping all data-fetching and API logic from `@btst/stack`:
120
+ Every capability below is installable from the released CLI. Follow its documentation for the actual payload, services, storage, auth, and adapter prerequisites.
238
121
 
239
- ```bash
240
- # Install a single plugin's UI (for example, Media)
241
- npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-media.json
122
+ | Capability | Topology | Outcome |
123
+ | --- | --- | --- |
124
+ | [Blog](https://www.better-stack.ai/docs/plugins/blog) | Full-stack | Publishing workflow, routes, API, data model, editor, metadata, and sitemap entries |
125
+ | [AI Chat](https://www.better-stack.ai/docs/plugins/ai-chat) | Full-stack | Streaming conversations, model integration, history, routes, and chat UI |
126
+ | [CMS](https://www.better-stack.ai/docs/plugins/cms) | Full-stack | Typed content models, APIs, generated forms, and editorial UI |
127
+ | [Form Builder](https://www.better-stack.ai/docs/plugins/form-builder) | Full-stack | Form authoring, validation, rendering, and submissions |
128
+ | [Kanban](https://www.better-stack.ai/docs/plugins/kanban) | Full-stack | Boards, columns, tasks, assignment, and drag-and-drop UI |
129
+ | [Comments](https://www.better-stack.ai/docs/plugins/comments) | Full-stack | Threads, replies, reactions, moderation, and embeddable UI |
130
+ | [Media](https://www.better-stack.ai/docs/plugins/media) | Full-stack | Media storage, library routes, uploads, folders, and picker UI |
131
+ | [UI Builder](https://www.better-stack.ai/docs/plugins/ui-builder) | Client-only · requires CMS | Visual page authoring and public rendering over CMS content |
132
+ | [OpenAPI](https://www.better-stack.ai/docs/plugins/open-api) | Backend-only | Generated OpenAPI document and interactive API reference endpoint |
133
+ | [Route Docs](https://www.better-stack.ai/docs/plugins/route-docs) | Client-only | Generated route reference and navigation UI |
134
+ | [Better Auth UI](https://www.better-stack.ai/docs/plugins/better-auth-ui) | Companion · requires Better Auth | Auth and account pages for an existing Better Auth backend |
242
135
 
243
- # Or install the full collection
244
- npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/registry.json
245
- ```
136
+ [Explore the released catalog](https://www.better-stack.ai/docs/plugins) or compare complete generated setups in the [interactive playground](https://www.better-stack.ai/playground?plugins=blog,ai-chat,comments&framework=nextjs&view=preview).
246
137
 
247
- Components are copied into `src/components/btst/{plugin}/client/` — all relative imports remain valid and you can edit them freely.
138
+ ## Compatibility and prerequisites
248
139
 
249
- ---
140
+ The maintained and tested v3 integration paths are:
250
141
 
251
- ## AI Agent Skills
142
+ | Framework | Maintained integration |
143
+ | --- | --- |
144
+ | Next.js 15+ App Router | Route handlers, request-aware and static pages, metadata, and sitemap factories |
145
+ | React Router v7 | Framework routes, SSR loaders, navigation, metadata, and sitemap response helpers |
146
+ | TanStack Start | File routes, SSR loaders, navigation, metadata, and sitemap response helpers |
252
147
 
253
- If you're using an AI coding agent (Cursor, Claude Code, VS Code, OpenAI Codex etc.) you can install the BTST integration skill so your agent understands the plugin system, adapter setup, and wiring patterns out of the box:
148
+ Other React integrations may be possible through custom adapters, but they are not part of that maintained matrix.
254
149
 
255
- ```bash
256
- npx skills@latest add better-stack-ai/better-stack/.agents/skills/btst-integration
257
- ```
258
-
259
- Or manually copy [`skills/btst-integration/SKILL.md`](./.agents/skills/btst-integration/SKILL.md) into your project's agent skills directory.
260
-
261
- ---
150
+ Versioned adapters are published for Prisma, Drizzle, Kysely, and MongoDB. Support is not blanket-equivalent: plugins that need isolated transactions have stricter persistent-adapter requirements, and the generated Form Builder and Media configurations reject MongoDB. The memory adapter is local and single-process only. Check [compatibility and prerequisites](https://www.better-stack.ai/docs/installation#compatibility-and-prerequisites) and the [adapter guide](https://www.better-stack.ai/docs/databases/adapters) for current versions and limits.
262
151
 
263
- ## Live Demo
152
+ ## Keep the view code, too
264
153
 
265
- Try the interactive playground:
154
+ BTST's packaged runtime keeps data fetching and behavior upgradeable. When a plugin offers a shadcn v4 registry block, it copies the view layer into your application. Edit that code freely, then pass pages back through the plugin's component overrides where available or render copied components directly; hooks and API behavior can remain package dependencies.
266
155
 
267
- * [Next.js](https://www.better-stack.ai/playground?plugins=blog,ai-chat,comments&framework=nextjs&view=preview)
268
- * [React Router](https://www.better-stack.ai/playground?plugins=blog,ai-chat,comments&framework=react-router&view=preview)
269
- * [TanStack Router](https://www.better-stack.ai/playground?plugins=blog,ai-chat,comments&framework=tanstack&view=preview)
156
+ [Read the ejection guide](https://www.better-stack.ai/docs/shadcn-registry)
270
157
 
271
- ---
158
+ ## Go deeper
272
159
 
273
- ## Learn more
160
+ - [Installation and production setup](https://www.better-stack.ai/docs/installation) — frameworks, providers, auth, origins, adapters, and migrations
161
+ - [How BTST works](https://www.better-stack.ai/docs/how-it-works) — backend/client composition and ownership boundaries
162
+ - [CLI reference](https://www.better-stack.ai/docs/cli) — initialize, generate schemas, migrate, and seed
163
+ - [Build a plugin](https://www.better-stack.ai/docs/plugins/development) — create a backend-only, client-only, or full-stack capability
164
+ - [API reference](https://www.better-stack.ai/docs/api-reference) — stack factories, runtime services, and framework adapters
165
+ - [Stable-v3 migration guide](https://www.better-stack.ai/docs/breaking-changes) — migrate older integrations to the released contract
274
166
 
275
- Full documentation, guides, and plugin development:
276
- 👉 **[https://www.better-stack.ai](https://www.better-stack.ai)**
167
+ ### Optional AI-agent guidance
277
168
 
278
- ---
169
+ After you understand the human installation path, you can give a coding agent the repository's BTST integration skill:
279
170
 
280
- ## Contributing
171
+ ```bash
172
+ npx skills@latest add better-stack-ai/better-stack/.agents/skills/btst-integration
173
+ ```
281
174
 
282
- Bug reports, plugin PRs, and documentation improvements are welcome.
283
- See **[CONTRIBUTING.md](./CONTRIBUTING.md)** for the plugin development guide, testing instructions, and submission checklist.
175
+ ## Contributing and license
284
176
 
285
- ---
177
+ Bug reports, plugin contributions, and documentation improvements are welcome. Read [CONTRIBUTING.md](./CONTRIBUTING.md) for the development workflow, tests, and submission checklist; public-facing copy follows the [BTST message and claims contract](./docs/positioning.md).
286
178
 
287
- If this saves you time, a ⭐ helps others find it.
179
+ BTST is released under the [MIT License](./LICENSE.md).
288
180
 
289
- MIT © [olliethedev](https://github.com/olliethedev)
181
+ If BTST is useful to you, a star helps other React developers find it.
@@ -3,7 +3,7 @@
3
3
 
4
4
  const jsxRuntime = require('react/jsx-runtime');
5
5
  const context = require('@btst/stack/context');
6
- const dateFns = require('date-fns');
6
+ const formatPostDate = require('../shared/format-post-date.cjs');
7
7
  const blogHooks = require('../../hooks/blog-hooks.cjs');
8
8
  const emptyList = require('../shared/empty-list.cjs');
9
9
  const markdownContent = require('../shared/markdown-content.cjs');
@@ -122,7 +122,7 @@ ${post.content ?? ""}`.slice(
122
122
  }
123
123
  function PostHeaderTop({ post }) {
124
124
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-row items-center justify-center gap-2 flex-wrap mt-8", children: [
125
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-light text-muted-foreground text-sm", children: dateFns.formatDate(post.createdAt, "MMMM d, yyyy") }),
125
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-light text-muted-foreground text-sm", children: formatPostDate.formatPostDate(post.createdAt) }),
126
126
  /* @__PURE__ */ jsxRuntime.jsx(collapsibleTagList.CollapsibleTagList, { tags: post.tags ?? [] })
127
127
  ] });
128
128
  }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from 'react/jsx-runtime';
3
3
  import { useTranslate, usePluginOverrides, useStack } from '@btst/stack/context';
4
- import { formatDate } from 'date-fns';
4
+ import { formatPostDate } from '../shared/format-post-date.mjs';
5
5
  import { useSuspensePost, useNextPreviousPosts, useRecentPosts } from '../../hooks/blog-hooks.mjs';
6
6
  import { EmptyList } from '../shared/empty-list.mjs';
7
7
  import { MarkdownContent } from '../shared/markdown-content.mjs';
@@ -120,7 +120,7 @@ ${post.content ?? ""}`.slice(
120
120
  }
121
121
  function PostHeaderTop({ post }) {
122
122
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center justify-center gap-2 flex-wrap mt-8", children: [
123
- /* @__PURE__ */ jsx("span", { className: "font-light text-muted-foreground text-sm", children: formatDate(post.createdAt, "MMMM d, yyyy") }),
123
+ /* @__PURE__ */ jsx("span", { className: "font-light text-muted-foreground text-sm", children: formatPostDate(post.createdAt) }),
124
124
  /* @__PURE__ */ jsx(CollapsibleTagList, { tags: post.tags ?? [] })
125
125
  ] });
126
126
  }
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ const formatter = new Intl.DateTimeFormat("en-US", {
4
+ month: "long",
5
+ day: "numeric",
6
+ year: "numeric",
7
+ timeZone: "UTC"
8
+ });
9
+ function formatPostDate(date) {
10
+ return formatter.format(new Date(date));
11
+ }
12
+
13
+ exports.formatPostDate = formatPostDate;
@@ -0,0 +1,11 @@
1
+ const formatter = new Intl.DateTimeFormat("en-US", {
2
+ month: "long",
3
+ day: "numeric",
4
+ year: "numeric",
5
+ timeZone: "UTC"
6
+ });
7
+ function formatPostDate(date) {
8
+ return formatter.format(new Date(date));
9
+ }
10
+
11
+ export { formatPostDate };
@@ -5,7 +5,7 @@ const jsxRuntime = require('react/jsx-runtime');
5
5
  const badge = require('../../../../../../../ui/src/components/badge.cjs');
6
6
  const card = require('../../../../../../../ui/src/components/card.cjs');
7
7
  const context = require('@btst/stack/context');
8
- const dateFns = require('date-fns');
8
+ const formatPostDate = require('./format-post-date.cjs');
9
9
  const LucideIcons = require('lucide-react');
10
10
  const constants = require('../../constants.cjs');
11
11
  const defaults = require('./defaults.cjs');
@@ -214,10 +214,8 @@ function PostCard({ post }) {
214
214
  const Image = router?.Image ?? defaults.DefaultImage;
215
215
  const basePath = context.useBasePath();
216
216
  const blogPath = `${basePath}/blog/${post.slug}`;
217
- const postDate = dateFns.formatDate(
218
- post.publishedAt || post.createdAt,
219
- "MMMM d, yyyy"
220
- );
217
+ const timestamp = post.publishedAt || post.createdAt;
218
+ const postDate = formatPostDate.formatPostDate(timestamp);
221
219
  const gradient = post.image ? null : getGradientFromTitle(post.title);
222
220
  return /* @__PURE__ */ jsxRuntime.jsxs(card.Card, { className: "group relative flex h-full flex-col gap-4 pt-0! pb-4! transition-shadow duration-200 hover:shadow-lg", children: [
223
221
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -264,7 +262,7 @@ function PostCard({ post }) {
264
262
  /* @__PURE__ */ jsxRuntime.jsx(card.CardContent, { className: "flex flex-1 flex-col gap-2", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
265
263
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-muted-foreground text-xs", children: [
266
264
  /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.CalendarIcon, { className: "h-3 w-3" }),
267
- /* @__PURE__ */ jsxRuntime.jsx("time", { dateTime: postDate, children: postDate })
265
+ /* @__PURE__ */ jsxRuntime.jsx("time", { dateTime: timestamp, children: postDate })
268
266
  ] }),
269
267
  post.tags && post.tags.length > 0 && post.tags.map((tag) => /* @__PURE__ */ jsxRuntime.jsx(badge.Badge, { variant: "secondary", className: "text-xs", children: tag.name }, tag.id))
270
268
  ] }) })
@@ -3,7 +3,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
3
3
  import { Badge } from '../../../../../../../ui/src/components/badge.mjs';
4
4
  import { Card, CardHeader, CardTitle, CardContent } from '../../../../../../../ui/src/components/card.mjs';
5
5
  import { useTranslate, usePluginOverrides, useStack, useBasePath } from '@btst/stack/context';
6
- import { formatDate } from 'date-fns';
6
+ import { formatPostDate } from './format-post-date.mjs';
7
7
  import { CalendarIcon } from 'lucide-react';
8
8
  import { BLOG_PLUGIN_ID } from '../../constants.mjs';
9
9
  import { DefaultLink, DefaultImage } from './defaults.mjs';
@@ -212,10 +212,8 @@ function PostCard({ post }) {
212
212
  const Image = router?.Image ?? DefaultImage;
213
213
  const basePath = useBasePath();
214
214
  const blogPath = `${basePath}/blog/${post.slug}`;
215
- const postDate = formatDate(
216
- post.publishedAt || post.createdAt,
217
- "MMMM d, yyyy"
218
- );
215
+ const timestamp = post.publishedAt || post.createdAt;
216
+ const postDate = formatPostDate(timestamp);
219
217
  const gradient = post.image ? null : getGradientFromTitle(post.title);
220
218
  return /* @__PURE__ */ jsxs(Card, { className: "group relative flex h-full flex-col gap-4 pt-0! pb-4! transition-shadow duration-200 hover:shadow-lg", children: [
221
219
  /* @__PURE__ */ jsx(
@@ -262,7 +260,7 @@ function PostCard({ post }) {
262
260
  /* @__PURE__ */ jsx(CardContent, { className: "flex flex-1 flex-col gap-2", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
263
261
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-muted-foreground text-xs", children: [
264
262
  /* @__PURE__ */ jsx(CalendarIcon, { className: "h-3 w-3" }),
265
- /* @__PURE__ */ jsx("time", { dateTime: postDate, children: postDate })
263
+ /* @__PURE__ */ jsx("time", { dateTime: timestamp, children: postDate })
266
264
  ] }),
267
265
  post.tags && post.tags.length > 0 && post.tags.map((tag) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs", children: tag.name }, tag.id))
268
266
  ] }) })
@@ -1,10 +1,10 @@
1
- export { B as BLOG_QUERY_KEYS, i as BlogApiRouter, k as BlogBackendHooks, f as BlogBackendOptions, l as BlogCreateErrorContext, m as BlogCreateOperationContext, n as BlogCreateResultContext, o as BlogDeleteErrorContext, p as BlogDeleteOperationContext, q as BlogDeleteResultContext, r as BlogListErrorContext, s as BlogListOperationContext, t as BlogListResultContext, u as BlogNextPreviousErrorContext, v as BlogNextPreviousOperationContext, w as BlogNextPreviousResultContext, e as BlogRouteKey, x as BlogUpdateErrorContext, y as BlogUpdateOperationContext, z as BlogUpdateResultContext, C as CreatePostOperationInputSchema, N as NextPreviousPostsQuerySchema, P as PostListParams, j as PostListQuerySchema, c as PostListResult, S as SerializedNextPreviousPostsResult, A as SerializedPostListResult, U as UpdatePostOperationInputSchema, h as blogBackendPlugin, d as createBlogQueryKeys, g as getAllPosts, b as getAllTags, a as getPostBySlug } from '../../../shared/stack.DiYg7Jw1.cjs';
1
+ export { B as BLOG_QUERY_KEYS, i as BlogApiRouter, k as BlogBackendHooks, f as BlogBackendOptions, l as BlogCreateErrorContext, m as BlogCreateOperationContext, n as BlogCreateResultContext, o as BlogDeleteErrorContext, p as BlogDeleteOperationContext, q as BlogDeleteResultContext, r as BlogListErrorContext, s as BlogListOperationContext, t as BlogListResultContext, u as BlogNextPreviousErrorContext, v as BlogNextPreviousOperationContext, w as BlogNextPreviousResultContext, e as BlogRouteKey, x as BlogUpdateErrorContext, y as BlogUpdateOperationContext, z as BlogUpdateResultContext, C as CreatePostOperationInputSchema, N as NextPreviousPostsQuerySchema, P as PostListParams, j as PostListQuerySchema, c as PostListResult, S as SerializedNextPreviousPostsResult, A as SerializedPostListResult, U as UpdatePostOperationInputSchema, h as blogBackendPlugin, d as createBlogQueryKeys, g as getAllPosts, b as getAllTags, a as getPostBySlug } from '../../../shared/stack.CtrnoQgH.cjs';
2
2
  import { DBAdapter } from '@btst/db';
3
3
  import { P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../../shared/stack.Dsatjh0B.cjs';
4
4
  export { blogPermissions } from '../permissions.cjs';
5
5
  import '@btst/stack/plugins/client';
6
6
  import 'zod';
7
- import '../../../shared/stack.DtZgEX84.cjs';
7
+ import '../../../shared/stack.CKc_Bais.cjs';
8
8
  import '@btst/stack/plugins/api';
9
9
  import '../../../authorization/index.cjs';
10
10
  import 'zod/v4/core';
@@ -1,10 +1,10 @@
1
- export { B as BLOG_QUERY_KEYS, i as BlogApiRouter, k as BlogBackendHooks, f as BlogBackendOptions, l as BlogCreateErrorContext, m as BlogCreateOperationContext, n as BlogCreateResultContext, o as BlogDeleteErrorContext, p as BlogDeleteOperationContext, q as BlogDeleteResultContext, r as BlogListErrorContext, s as BlogListOperationContext, t as BlogListResultContext, u as BlogNextPreviousErrorContext, v as BlogNextPreviousOperationContext, w as BlogNextPreviousResultContext, e as BlogRouteKey, x as BlogUpdateErrorContext, y as BlogUpdateOperationContext, z as BlogUpdateResultContext, C as CreatePostOperationInputSchema, N as NextPreviousPostsQuerySchema, P as PostListParams, j as PostListQuerySchema, c as PostListResult, S as SerializedNextPreviousPostsResult, A as SerializedPostListResult, U as UpdatePostOperationInputSchema, h as blogBackendPlugin, d as createBlogQueryKeys, g as getAllPosts, b as getAllTags, a as getPostBySlug } from '../../../shared/stack.CZ1h_T4k.mjs';
1
+ export { B as BLOG_QUERY_KEYS, i as BlogApiRouter, k as BlogBackendHooks, f as BlogBackendOptions, l as BlogCreateErrorContext, m as BlogCreateOperationContext, n as BlogCreateResultContext, o as BlogDeleteErrorContext, p as BlogDeleteOperationContext, q as BlogDeleteResultContext, r as BlogListErrorContext, s as BlogListOperationContext, t as BlogListResultContext, u as BlogNextPreviousErrorContext, v as BlogNextPreviousOperationContext, w as BlogNextPreviousResultContext, e as BlogRouteKey, x as BlogUpdateErrorContext, y as BlogUpdateOperationContext, z as BlogUpdateResultContext, C as CreatePostOperationInputSchema, N as NextPreviousPostsQuerySchema, P as PostListParams, j as PostListQuerySchema, c as PostListResult, S as SerializedNextPreviousPostsResult, A as SerializedPostListResult, U as UpdatePostOperationInputSchema, h as blogBackendPlugin, d as createBlogQueryKeys, g as getAllPosts, b as getAllTags, a as getPostBySlug } from '../../../shared/stack.BggTHKlo.mjs';
2
2
  import { DBAdapter } from '@btst/db';
3
3
  import { P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../../shared/stack.Dsatjh0B.mjs';
4
4
  export { blogPermissions } from '../permissions.mjs';
5
5
  import '@btst/stack/plugins/client';
6
6
  import 'zod';
7
- import '../../../shared/stack.DtZgEX84.mjs';
7
+ import '../../../shared/stack.CKc_Bais.mjs';
8
8
  import '@btst/stack/plugins/api';
9
9
  import '../../../authorization/index.mjs';
10
10
  import 'zod/v4/core';
@@ -1,10 +1,10 @@
1
- export { B as BLOG_QUERY_KEYS, i as BlogApiRouter, k as BlogBackendHooks, f as BlogBackendOptions, l as BlogCreateErrorContext, m as BlogCreateOperationContext, n as BlogCreateResultContext, o as BlogDeleteErrorContext, p as BlogDeleteOperationContext, q as BlogDeleteResultContext, r as BlogListErrorContext, s as BlogListOperationContext, t as BlogListResultContext, u as BlogNextPreviousErrorContext, v as BlogNextPreviousOperationContext, w as BlogNextPreviousResultContext, e as BlogRouteKey, x as BlogUpdateErrorContext, y as BlogUpdateOperationContext, z as BlogUpdateResultContext, C as CreatePostOperationInputSchema, N as NextPreviousPostsQuerySchema, P as PostListParams, j as PostListQuerySchema, c as PostListResult, S as SerializedNextPreviousPostsResult, A as SerializedPostListResult, U as UpdatePostOperationInputSchema, h as blogBackendPlugin, d as createBlogQueryKeys, g as getAllPosts, b as getAllTags, a as getPostBySlug } from '../../../shared/stack.DQpyA3Cq.js';
1
+ export { B as BLOG_QUERY_KEYS, i as BlogApiRouter, k as BlogBackendHooks, f as BlogBackendOptions, l as BlogCreateErrorContext, m as BlogCreateOperationContext, n as BlogCreateResultContext, o as BlogDeleteErrorContext, p as BlogDeleteOperationContext, q as BlogDeleteResultContext, r as BlogListErrorContext, s as BlogListOperationContext, t as BlogListResultContext, u as BlogNextPreviousErrorContext, v as BlogNextPreviousOperationContext, w as BlogNextPreviousResultContext, e as BlogRouteKey, x as BlogUpdateErrorContext, y as BlogUpdateOperationContext, z as BlogUpdateResultContext, C as CreatePostOperationInputSchema, N as NextPreviousPostsQuerySchema, P as PostListParams, j as PostListQuerySchema, c as PostListResult, S as SerializedNextPreviousPostsResult, A as SerializedPostListResult, U as UpdatePostOperationInputSchema, h as blogBackendPlugin, d as createBlogQueryKeys, g as getAllPosts, b as getAllTags, a as getPostBySlug } from '../../../shared/stack.lPKwd_v0.js';
2
2
  import { DBAdapter } from '@btst/db';
3
3
  import { P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../../shared/stack.Dsatjh0B.js';
4
4
  export { blogPermissions } from '../permissions.js';
5
5
  import '@btst/stack/plugins/client';
6
6
  import 'zod';
7
- import '../../../shared/stack.DtZgEX84.js';
7
+ import '../../../shared/stack.CKc_Bais.js';
8
8
  import '@btst/stack/plugins/api';
9
9
  import '../../../authorization/index.js';
10
10
  import 'zod/v4/core';
@@ -1,9 +1,9 @@
1
- export { P as PostCreateInput, e as PostUpdateInput, p as UseNextPreviousPostsOptions, q as UseNextPreviousPostsResult, d as UsePostResult, b as UsePostSearchOptions, c as UsePostSearchResult, U as UsePostsOptions, a as UsePostsResult, s as UseRecentPostsOptions, t as UseRecentPostsResult, l as useCreatePost, n as useDeletePost, r as useNextPreviousPosts, g as usePost, k as usePostForm, o as usePostSearch, u as usePosts, v as useRecentPosts, h as useSuspensePost, f as useSuspensePosts, j as useSuspenseTags, i as useTags, m as useUpdatePost } from '../../../../shared/stack.rmWRtSjs.cjs';
1
+ export { P as PostCreateInput, e as PostUpdateInput, p as UseNextPreviousPostsOptions, q as UseNextPreviousPostsResult, d as UsePostResult, b as UsePostSearchOptions, c as UsePostSearchResult, U as UsePostsOptions, a as UsePostsResult, s as UseRecentPostsOptions, t as UseRecentPostsResult, l as useCreatePost, n as useDeletePost, r as useNextPreviousPosts, g as usePost, k as usePostForm, o as usePostSearch, u as usePosts, v as useRecentPosts, h as useSuspensePost, f as useSuspensePosts, j as useSuspenseTags, i as useTags, m as useUpdatePost } from '../../../../shared/stack.BTStHVTy.cjs';
2
2
  export { useDebounce } from '@btst/stack/plugins/client/hooks';
3
3
  import '@tanstack/react-query';
4
4
  import 'zod';
5
5
  import '../../../../shared/stack.Dsatjh0B.cjs';
6
- import '../../../../shared/stack.DtZgEX84.cjs';
6
+ import '../../../../shared/stack.CKc_Bais.cjs';
7
7
 
8
8
  declare function useThrottle<T>(value: T, wait?: number): T;
9
9
 
@@ -1,9 +1,9 @@
1
- export { P as PostCreateInput, e as PostUpdateInput, p as UseNextPreviousPostsOptions, q as UseNextPreviousPostsResult, d as UsePostResult, b as UsePostSearchOptions, c as UsePostSearchResult, U as UsePostsOptions, a as UsePostsResult, s as UseRecentPostsOptions, t as UseRecentPostsResult, l as useCreatePost, n as useDeletePost, r as useNextPreviousPosts, g as usePost, k as usePostForm, o as usePostSearch, u as usePosts, v as useRecentPosts, h as useSuspensePost, f as useSuspensePosts, j as useSuspenseTags, i as useTags, m as useUpdatePost } from '../../../../shared/stack.D2y4gl03.mjs';
1
+ export { P as PostCreateInput, e as PostUpdateInput, p as UseNextPreviousPostsOptions, q as UseNextPreviousPostsResult, d as UsePostResult, b as UsePostSearchOptions, c as UsePostSearchResult, U as UsePostsOptions, a as UsePostsResult, s as UseRecentPostsOptions, t as UseRecentPostsResult, l as useCreatePost, n as useDeletePost, r as useNextPreviousPosts, g as usePost, k as usePostForm, o as usePostSearch, u as usePosts, v as useRecentPosts, h as useSuspensePost, f as useSuspensePosts, j as useSuspenseTags, i as useTags, m as useUpdatePost } from '../../../../shared/stack.94kN3pEK.mjs';
2
2
  export { useDebounce } from '@btst/stack/plugins/client/hooks';
3
3
  import '@tanstack/react-query';
4
4
  import 'zod';
5
5
  import '../../../../shared/stack.Dsatjh0B.mjs';
6
- import '../../../../shared/stack.DtZgEX84.mjs';
6
+ import '../../../../shared/stack.CKc_Bais.mjs';
7
7
 
8
8
  declare function useThrottle<T>(value: T, wait?: number): T;
9
9