Quick start
- Install
@btst/stack,@tanstack/react-query, and one@btst/adapter-*. - Install and register each plugin's backend and client halves.
- Create
lib/stack.ts→ export{ handler, dbSchema }. - Mount
handlerwith the framework API entry factory at/api/data/*. - Add
@import "@btst/stack/plugins/{plugin}/css"per plugin in your global CSS. - Create
lib/stack-client.tsx,lib/query-client.ts, and the/pages/*catch-all route with the framework page entry factory. - Create the pages layout file with
QueryClientProvider+StackProvider. - Run
@btst/cli generate(andmigratefor Kysely).
See REFERENCE.md for full code shapes for every file.
Workflow
1) Install prerequisites and packages
- Prereqs: shadcn/ui (CSS variables enabled), Sonner
<Toaster />, Tailwind v4. - Install:
@btst/stack,@tanstack/react-query, and one adapter:@btst/adapter-prisma/@btst/adapter-drizzle/@btst/adapter-kysely/@btst/adapter-mongodb@btst/adapter-memory— dev/testing only, not for production
- Install each plugin package alongside
@btst/stack(they ship inside the monorepo package).
2) Register plugins (backend + client)
- Backend (
lib/stack.ts): import{plugin}BackendPluginfrom@btst/stack/plugins/{plugin}/api.- Pass hooks and config in the backend plugin options object (e.g.
blogBackendPlugin({ hooks })). - Use camelCase keys for plugins that have compound names:
aiChat,formBuilder,uiBuilder.
- Pass hooks and config in the backend plugin options object (e.g.
- Client (
lib/stack-client.tsx): import{plugin}ClientPluginfrom@btst/stack/plugins/{plugin}/client.- Client definitions receive only plugin-specific options such as
seo, hooks, or page components. Configureapi,site, andqueryClientonce oncreateClientStack(); its resolver binds that runtime to every definition. - Pass incoming SSR request headers through the top-level
createClientStack({ api: { headers } })configuration. - Resolve one trusted API/site origin snapshot on the server and hydrate it into the provider. A managed backend may use a distinct
BTST_API_URL; never reconstruct it fromwindow.locationafter hydration.
- Client definitions receive only plugin-specific options such as
3) Configure backend stack
- Call
createBackendStack({ basePath: "/api/data", plugins: { ... }, adapter: (db) => createXxxAdapter(..., db, {}), auth: serverAuth })when application authorization is enabled. - Create
serverAuthwithcreateServerAuth({ authorization, getIdentity }). Omittingauthintentionally preserves permissive no-authorization compatibility; plugin labels such as AI Chat'saccess: "authorized"are not an enforcement boundary by themselves. - Export
handleranddbSchema. - Memory adapter + Next.js: pin to
globalThisto avoid two instances in the same process:function createAppStack() { return createBackendStack({ ... }) } type AppStack = ReturnType<typeof createAppStack> const g = globalThis as typeof globalThis & { __btst__?: AppStack } export const myStack = g.__btst__ ??= createAppStack() export const { handler, dbSchema } = myStack
4) CSS — edit global stylesheet
Add one line per plugin before your Tailwind layers:
@import "@btst/stack/plugins/blog/css";
@import "@btst/stack/plugins/cms/css";
@import "@btst/stack/plugins/ai-chat/css";
/* …one per selected plugin */
Do not duplicate — the patcher and manual edits must both be idempotent.
5) Wire framework routes and client runtime
- API route: use
toNextRouteHandlers,toReactRouterHandlers, ortoTanStackHandlersfrom the framework entry point. - Pages route: use
createNextPage,createReactRouterPage, orcreateTanStackPageOptions. The factory owns route matching, loader ordering, hydration, metadata, and 404 handling. - Pages layout (
"use client"in Next.js): wrap inQueryClientProviderthenStackProvider:- Pass the resolved client stack with
stack={clientStack}. Its top-levelsite.basePathmust match your pages catch-all prefix. router={nextRouter()}/reactRouter()/tanstackRouter()for framework-wide links, images, navigation, and refresh.auth={createClientAuth({ authorization, getIdentity, loginPath })}when plugins need identity or permission presentation gates.overrides={{ pluginKey: { uploadImage?, ...pluginSpecificValues } }}only for plugin-specific customization.- Let the resolved plugin map infer override keys and values; do not duplicate a manual provider map.
- See REFERENCE.md for provider wiring and per-plugin override shapes.
- Pass the resolved client stack with
- ai-chat plugin only: wrap the root layout (above
StackProvider) withPageAIContextProvider:
Pages then callimport { PageAIContextProvider } from "@btst/stack/plugins/ai-chat/client/context" // root layout.tsx (not the pages layout) export default function RootLayout({ children }) { return <PageAIContextProvider>{children}</PageAIContextProvider> }useRegisterPageAIContext(...)to register their AI context — see thebtst-ai-contextskill.
6) Generate schemas and run migrations
- Install
@btst/clias a dev dependency. - Prisma:
npx @btst/cli generate --config=lib/stack.ts --orm=prisma --output=schema.prisma - Drizzle:
npx @btst/cli generate --config=lib/stack.ts --orm=drizzle --output=src/db/schema.ts - Kysely:
npx @btst/cli generate --config=lib/stack.ts --orm=kysely --output=migrations/schema.sql --database-url=... - Kysely migrate:
npx @btst/cli migrate --config=lib/stack.ts --database-url=... - Prisma/Drizzle: use native migration tooling after generate.
Validation checklist
stack.tsexports bothhandleranddbSchema.- Protected applications pass
createServerAuth(...)tocreateBackendStack({ auth }); omitted auth is intentionally permissive. - Every plugin is registered on both backend and client sides.
- API
basePathandcreateBackendStack({ basePath })match exactly. - API and page catch-all routes use the framework entry factories.
- Pages layout is
"use client"and wrapsQueryClientProviderthenStackProvider. - The resolved client stack's
site.basePathmatches the/pagescatch-all route prefix. - Global CSS has one
@importline per selected plugin. /pages/*routes render expected plugin pages.- CLI commands run with required env vars in scope.
Gotchas
"use client"missing on pages layout —StackProvideruses hooks and must be client-side.- Wrong key casing — backend/client plugin map keys must match; compound names use camelCase (
aiChat, notai-chat). - Missing CSS import — plugin UI breaks silently even when data loads correctly.
- Half-registered plugin — backend-only = routes don't exist; client-only = 404 on data calls.
- Memory adapter + Next.js — always pin to
globalThisto share one in-memory store across API and page bundles. - Path aliases in CLI —
@btst/cliexecutes your config file directly; use relative imports inlib/stack.tsand its dependencies. - Kysely generate needs DB — pass
DATABASE_URLor--database-url; usedotenv-clifor.env.local. - Provider services copied into plugin overrides — never add
Link,Image, navigation, refresh, API paths, identity, or login values to built-in plugin overrides. Use top-levelrouter,api, andauth.