Trace how BTST v3 connects backend operations, client routes, and SSR loaders, with request isolation, hydration, and application responsibilities explained.

BTST splits a full-stack feature into backend and client plugins. The backend provides operations, HTTP endpoints, and database requirements; the client provides page routes, loaders, and React components. Your application connects them to its framework, authentication, database, and deployment.
This guide traces that boundary using the Blog plugin in @btst/stack 3.1.2. It explains where data and credentials travel, what server rendering does, and which parts you still need to implement. For a complete framework setup, use the Next.js Blog guide, TanStack Start guide, or React Router guide.
A backend registration creates the server side of the feature. This example uses memory storage to show the shape of the API:
// lib/stack.ts — local demonstration only
import { createBackendStack } from "@btst/stack/api"
import { createMemoryAdapter } from "@btst/adapter-memory"
import { blogBackendPlugin } from "@btst/stack/plugins/blog/api"
export const stack = createBackendStack({
basePath: "/api/data",
plugins: {
blog: blogBackendPlugin(),
},
adapter: (schema) => createMemoryAdapter(schema)({}),
})
export const { handler, dbSchema } = stack
This is a local configuration: memory data is temporary, and no authorization policy is configured. Anonymous writes are possible with this example. Before exposing the handler publicly, configure persistent storage and server authorization. The installation documentation covers project setup; authorization documentation covers the application's access rules.
handler handles API requests at the configured path. dbSchema describes the composed database requirements. An adapter connects operations to storage; it does not provision a database or apply production migrations automatically.
Client registration is separate. In v3, shared API and site locations belong to createClientStack, rather than being repeated inside each plugin:
// lib/stack-client.ts
import { createClientStack } from "@btst/stack/client"
import { blogClientPlugin } from "@btst/stack/plugins/blog/client"
import type { QueryClient } from "@tanstack/react-query"
export function getStackClient(
queryClient: QueryClient,
requestHeaders?: Headers,
) {
return createClientStack({
api: {
baseURL: "http://localhost:3000",
basePath: "/api/data",
headers: requestHeaders,
},
site: {
baseURL: "http://localhost:3000",
basePath: "/pages",
},
queryClient,
plugins: {
blog: blogClientPlugin(),
},
})
}
The localhost URLs describe a local app. Configure the intended API and public site origins for deployment. api.basePath tells loaders where to fetch data; site.basePath tells the plugin where its pages live. Mount both paths in your framework. Registering a plugin does not create those framework files for you.
For /pages/blog/example-post, the framework first matches your page route. Its integration normalizes the remaining path to /blog/example-post, creates the client stack, and asks stackClient.router.getRoute(path) for the plugin route.
The route can supply a loader, a PageComponent, and metadata. The framework integration calls the loader and renders the component. The backend API handles the loader's request, applies server access rules, and uses the configured adapter to read content.
| Layer | Responsibility |
|---|---|
| Framework route | Receive the request, establish request context, render or return a response |
| BTST page router | Match a plugin page and expose its loader, component, and metadata |
| BTST API handler | Dispatch an API request to the backend feature |
| Backend operation and adapter | Validate and authorize the operation, then access storage |
The client stack can run on the server to load data. Here, “client” identifies the feature's page and UI layer; it does not mean all its work happens in the browser.
A request-scoped query cache holds loader results. The integration dehydrates eligible query state for transfer to the browser, where React Query can use it when rendering the same feature. These are separate steps from React attaching interactive behavior to server-rendered HTML. TanStack Query's SSR guide describes this lifecycle.
This helper demonstrates route loading and cache preparation. It is not a complete framework page: the caller still handles missing routes, renders the component, provides the query client, and integrates hydration.
// lib/load-blog-route.ts — called on the server
import { QueryClient, dehydrate } from "@tanstack/react-query"
import { getStackClient } from "./stack-client"
export async function loadBlogRoute(
path: string,
requestHeaders?: Headers,
) {
const queryClient = new QueryClient()
const stackClient = getStackClient(queryClient, requestHeaders)
const route = stackClient.router.getRoute(path)
if (!route) return null
if (route.loader) await route.loader()
return { route, dehydratedState: dehydrate(queryClient) }
}
Keep the returned route on the server; it contains executable component and loader values. Transfer only the appropriate serializable state through your framework's hydration mechanism.
Prefetching can put content in the initial HTML and avoid an initial browser fetch. It does not guarantee instant hydration, eliminate every request waterfall, or prevent refetching. Query freshness, errors, uncached data, streaming, and the framework's rendering choices still matter. Check the actual HTML and network behavior of the routes you ship.
Use a separate server query cache for each request. A process-wide cache can expose one user's data to another request. The browser should have its own stable query client for the relevant session lifecycle.
When a server loader calls your own authenticated API, supply the credentials required for that request through api.headers. Forward credentials only to the intended trusted API origin. Create the browser client stack separately without server request headers; do not serialize the request-scoped stack into browser props. The released client API provides a header-free provider projection, but that alone does not make cached query results safe to disclose.
Only dehydrate data the current viewer may receive. Clear or partition identity-dependent browser state when accounts change. Hiding an editor button or a client route is a presentation choice; the server must independently reject unauthorized reads and writes. The drafts and caching guide explains the related publishing boundaries.
BTST v3 uses StackProvider from @btst/stack/context with the resolved browser stack passed through its stack prop. Framework router integrations provide navigation behavior; plugin overrides handle feature-specific UI needs. Follow the matching framework guide for provider placement and hydration rather than copying older BetterStackProvider examples.
Keep backend modules and database credentials out of browser imports. The browser needs the client stack and UI configuration; it should reach server operations through the API boundary. Separate entry points help maintain that boundary, but your imports and bundler configuration still need verification.
The app also owns image upload authorization and storage, plugin CSS imports, public URL configuration, and any framework-specific cache refresh after a write. A Blog editor does not supply a production upload service simply because an upload callback exists.
Test real requests: a public reader can load a published post, a forbidden write fails at the API, and draft content stays private. Then inspect the rendered article, hydration, authenticated navigation, feature images, canonical URL, and sitemap.
Plugin metadata and sitemap helpers provide inputs for framework integration. They do not submit pages to search engines or guarantee indexing. Likewise, a type-correct adapter configuration does not establish that migrations, storage permissions, and the deployment runtime work together.
This guide's configuration and loader examples were checked against the Stack 3.1.2 release. Start with the framework guides above for implementation, or review the BTST source for the operation and routing contracts.