BTST

How It Works

Understand the architecture and key concepts behind BTST.

Here's a high-level overview of how BTST works:

Diagram separating the application and operating boundaries a team owns from the BTST runtimes and plugins running inside it.

BTST composes client and backend plugins inside your application. Your app shell, ejected views, database, deployment, and external services remain yours.

The current Blog backend and client registrations point to the real published Blog route they produce.

Register Blog with the backend and client stacks, then inspect the resulting route at /pages/blog.

Server Side

The server handles database operations, API endpoints, data prefetching, routing, and server-side rendering.

createBackendStack manages the backend layer:

  • API Router: Routes incoming requests to the appropriate plugin handlers. Returns a handler function that you mount at your API path.
  • DB Adapter: Translates BTST's database operations to your ORM (Prisma, Drizzle, Kysely, MongoDB). Plugins define schemas that get merged and passed to the adapter.

createClientStack manages the rendering layer:

  • Data Fetching: Plugins can prefetch data server-side into React Query cache before rendering, enabling instant page loads with hydrated state.
  • Page Router: Matches URLs to plugin routes and returns the appropriate page component, loader, and metadata.
  • SSR: Server-side renders pages with prefetched data, then hydrates on the client.

Client Side

After server-side rendering, the client takes over for interactivity.

React Hydration

Server-rendered HTML is hydrated with client-side React. The React Query cache—prefetched during SSR—transfers seamlessly, so components render instantly without loading states or refetching.

SPA Navigation (If using in an SPA)

After the initial page load, the framework router preset on StackProvider handles client-side navigation. React Query fetches data in the background while the UI updates.

State Management

First party plugins use React Query under the hood for all data operations:

  • Queries: Hooks like usePosts(), usePost(slug), and useTags() (examples from the blog plugin) fetch and cache data with automatic background refetching
  • Mutations: Hooks like useCreatePost(), useUpdatePost(), and useDeletePost() (examples from the blog plugin) handle writes with automatic cache invalidation
  • Suspense: Suspense variants (useSuspensePosts, useSuspensePost) integrate with React Suspense boundaries for streaming SSR and other advanced features

Note: 3rd party plugins may use a different state management library.

Context & Overrides

The StackProvider wraps your pages with a resolved client stack plus browser-only router and auth services. API, site, QueryClient, and plugin endpoint values come from createClientStack(); plugin components read that projection and router services such as Link, Image, and navigate through useStack(). usePluginOverrides() is reserved for genuinely plugin-specific customization.

Plugins

Plugins are the building blocks of BTST. Full-stack features such as Blog ship separate backend and client definitions that you register independently:

// Backend: lib/stack.ts
import { createBackendStack } from "@btst/stack/api"
import { blogBackendPlugin } from "@btst/stack/plugins/blog/api"

const { handler } = createBackendStack({
  plugins: {
    blog: blogBackendPlugin({ /* config */ })
  },
  // ...
})

// Client: lib/stack-client.tsx
import { createClientStack } from "@btst/stack/client"
import { blogClientPlugin } from "@btst/stack/plugins/blog/client"
import { QueryClient } from "@tanstack/react-query"

const queryClient = new QueryClient()
const stackClient = createClientStack({
  api: { baseURL: "https://example.com", basePath: "/api/data" },
  site: { baseURL: "https://example.com", basePath: "/pages" },
  queryClient,
  plugins: {
    blog: blogClientPlugin({ /* config */ })
  }
})

Backend plugins (registered in createBackendStack):

  • Define database schemas (tables, columns, relations)
  • Register API route handlers for CRUD operations
  • Provide post-authorization domain lifecycle hooks

Client plugins (registered in createClientStack):

  • Define page routes and components
  • Provide loaders for server-side data prefetching
  • Export components, hooks, and utilities for state management
  • Generate SEO metadata and sitemaps

This separation keeps server-only code (database schemas, API handlers) out of your client bundle, and allows each plugin to be configured independently for its context.

One-sided plugins are intentional: OpenAPI is backend-only, Route Docs is client-only, and UI Builder is client-only over the CMS backend/client contract. Register only the side that exists; do not add a placeholder half.

Resolved Client Runtime and Provider Services

Configure shared runtime once and pass the resolved result to the provider:

const clientStack = createClientStack({
  api,
  site,
  queryClient,
  plugins: { blog: blogClientPlugin() },
})

<StackProvider
  stack={clientStack}
  router={nextRouter()}
  auth={clientAuth}
  overrides={{ blog: { uploadImage } }}
>
  {children}
</StackProvider>
  • stack: Supplies the API and site locations, QueryClient, browser-safe per-plugin endpoints, and exact plugin map. That map also infers valid overrides keys and values.

  • router: Use nextRouter(), reactRouter(), or tanstackRouter() for links, images, navigation, refresh, and URL search state.

  • auth: Resolve identity, provide a login path, and evaluate exact schema-backed permission descriptors.

The overrides object is only for plugin-specific customization such as upload functions, component slots, localization, and route analytics. SSR loader hooks, page choices, and metadata customization remain plugin-specific factory options; their shared transport and cache runtime arrives through the resolved stack.