BTST
PluginsQuickstartDocs
Live Blog

From research to product evaluation

Evaluating a publishing workflow for an app you already own?

See what the BTST Blog plugin adds to an existing React or Next.js app
BTST

Open-source TypeScript features for the React application, data, and deployment you already own.

Released plugins

  • Blog
  • AI Chat
  • CMS
  • Form Builder
  • UI Builder
  • Kanban
  • Comments
  • Media
  • Route Docs
  • OpenAPI
  • Better Auth UI

Resources

  • Quickstart
  • Documentation
  • All plugins
  • Live Blog
  • GitHub (opens in a new tab)
© 2026 BTST. Open source under the MIT License.
AI Chat
September 15, 2026NextJSReact

BTST in Next.js: Separate Request and Static Route Groups

Keep authenticated plugin pages and public static content under compatible layouts, with request credentials and query hydration in the right scope.

BTST in Next.js: Separate Request and Static Route Groups

A BTST application can have public blog pages built ahead of time and account or management pages rendered for the current visitor. In Next.js, those pages need different layout boundaries even when their URLs share a /p prefix.

This guide explains the @btst/stack@3.0.2 helpers for the Next.js App Router with Cache Components disabled. The released createNextLayout explicitly targets route-segment caching in that configuration. Treat an application using cacheComponents: true as a separate integration; do not assume adding dynamic = "force-dynamic" makes these examples apply unchanged.

Separate the physical route groups#

Use distinct route groups for the request-aware plugin catch-all and explicit public pages:

TEXT
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
app/
  (request)/
    p/
      layout.tsx
      [[...all]]/page.tsx
  (static)/
    p/
      layout.tsx
      blog/
        page.tsx
        [slug]/page.tsx
  p/
    pages-client-layout.tsx

The group names in parentheses organize layouts without appearing in the URL. The public Blog routes remain /p/blog and /p/blog/[slug]; the catch-all handles other plugin pages. The final app/p/pages-client-layout.tsx is a shared component module, not a route or a shared layout.tsx.

Do not define the same concrete page in both groups. Keep request identity resolution out of the common root layout if public pages must remain independent of the visitor. The released page helper documents this physical split.

Resolve identity in the request layout#

For an application with an existing server authorization adapter and shared client provider, the request layout can be small:

TSX
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
import { createNextLayout } from "@btst/stack/next/server";
import { hydrationStackAuth } from "@/lib/stack-authorization.server";
import { PagesStackProvider } from "@/app/p/pages-client-layout";

export const dynamic = "force-dynamic";

const { Layout } = createNextLayout({
  auth: hydrationStackAuth,
  ClientLayout: PagesStackProvider,
});

export default Layout;

hydrationStackAuth and PagesStackProvider are application modules, not package exports to install. Use your corresponding modules from the authorization setup. The provider must accept initialIdentity, pass it to StackProvider, and preserve the existing client authorization adapter.

The layout implementation reads request headers, resolves a validated identity, and passes that snapshot to the client boundary. Import it from the server entry point so the header-reading code stays in the server module graph. Do not serialize session tokens or raw request headers into client props.

Give the page a request-aware stack too#

The page loader needs the current request's API credentials as well as a request-scoped query client. For an installation with the generated server stack factory:

TSX
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
import { createNextPage } from "@btst/stack/next";
import { headers } from "next/headers";
import { getOrCreateQueryClient } from "@/lib/query-client";
import { getStackClientForRequest } from "@/lib/stack-client.server";

export const dynamic = "force-dynamic";

const page = createNextPage({
  getStackClient: async (queryClient) =>
    getStackClientForRequest(queryClient, {
      headers: new Headers(await headers()),
    }),
  getQueryClient: getOrCreateQueryClient,
});

export default page.Page;
export const generateMetadata = page.generateMetadata;

getStackClientForRequest comes from your generated or equivalent server integration. The released server factory template resolves trusted API/site origins from deployment configuration and filters headers before forwarding credentials. Preserve those checks. A request's Host or forwarding header must not choose the destination that receives session credentials in production.

The helper runs the matched route loader before rendering and also before route metadata. A successful page prefetch alone does not prove the provider is using the corresponding stack. Keep the API mount, plugin set, identity partitioning where required, and query-client ownership consistent through prefetch and hydration.

On the server, use a query client scoped to the request rather than a process-wide singleton. The browser can retain its own query client across navigation. Preserve BTST's identity-change handling so cached account data is not carried into a different user's session.

Keep static pages anonymous#

The static layout can reuse the shared client provider with an anonymous initial identity. Its server path must not resolve a session from headers() or cookies(). A browser identity refresh after hydration can enable account controls, but the HTML and dehydrated data produced ahead of time must already be suitable for any visitor.

Static is a rendering choice, not an access-control policy. Select public content explicitly before prefetching it. Blog raw getters bypass operation authorization; management collections need request-authorized reads. See the public drafts and caching guide for that data boundary.

Diagnose the boundary before changing permissions#

ObservationCheck
Server prefetch succeeds, then a request gets 401Which stack owns the later query, and whether it has the expected identity and credentials
A public page reads request headers during renderingParent layouts and imported server helpers
Hydration immediately refetches protected dataQuery keys, query-client ownership, provider setup, and intentional stale/refetch settings
One account sees another account's cached resultServer query-client lifetime and browser identity partition/reset behavior

A 401 can be the correct authorization response to a later anonymous request. Inspect the concrete request path before changing backend permissions. The product's tracked composition issue records this failure pattern; its proposed helper names and diagnostics are future work, not APIs used in this guide.

Verify with a production build, an anonymous public page, a direct authenticated management-page load, browser navigation, and a sign-out/account-switch sequence. The examples were type-checked against the stated integration; they are not a substitute for testing your application's session and data policy.

Use the installation documentation for complete framework wiring and the Better Auth UI integration guide for session/provider setup.

In This Post

Separate the physical route groupsResolve identity in the request layoutGive the page a request-aware stack tooKeep static pages anonymousDiagnose the boundary before changing permissions