Routing & Data
Route Guards
Route Guards — Routing & Data documentation for BCP Framework.
Last updated: 2026-08-29T01:11:41
Protected Route Guards
BCP route guards provide server-side protection for pages and page subtrees without repeating authentication checks in every loader.ts.
A guard lives in guard.ts or guard.tsx inside the app/ tree. It protects pages in that directory and descendant route directories.
app/
└─ dashboard/
├─ guard.ts
├─ page.tsx
└─ users/
└─ [id]/
├─ loader.ts
└─ page.tsxAuthentication guard
// app/dashboard/guard.ts
import {
getSession,
redirect,
} from "bcp/server";
export async function guard() {
const session =
await getSession<{
userId: number;
email: string;
role: string;
}>();
if (!session) {
return redirect(
"/login",
303
);
}
return {
session,
role:
session.role,
};
}The guard runs before the route loader and page render. Returning a Web Response, including redirect(), stops the pipeline immediately.
Guard context
A guard receives:
interface GuardContext {
params: Record<string, string | string[] | undefined>;
searchParams: URLSearchParams;
parentData: Readonly<Record<string, unknown>>;
}params contains the matched route params and searchParams represents the target page URL. parentData contains the JSON-safe values returned by ancestor guards.
Example nested role guard:
// app/dashboard/admin/guard.ts
import {
redirect,
} from "bcp/server";
export async function guard({
parentData,
}) {
if (
parentData.role !==
"admin"
) {
return redirect(
"/dashboard",
303
);
}
return {
section:
"admin",
};
}Guards execute from the app root toward the page directory. Later guards receive the merged output of earlier guards.
Using guard data in a loader
Loaders receive the final merged guard data as guardData:
// app/dashboard/users/[id]/loader.ts
export async function loader({
params,
guardData,
}) {
return {
id:
params.id,
currentUser:
guardData.session,
role:
guardData.role,
};
}This avoids calling getSession() again after the guard has already validated the request.
Using guard data in a page
Pages can read the same serialized guard result with useGuardData<T>():
"use client";
import {
useGuardData,
} from "bcp";
export default function DashboardPage() {
const guard =
useGuardData<{
session: {
userId: number;
email: string;
};
role: string;
}>();
return (
<main>
Signed in as {
guard.session.email
}
</main>
);
}Guard data is available during SSR, hydration and SPA navigation. A guarded page does not need a loader.ts just to expose guard data.
Serialization rules
Guard return values must be JSON-safe because they are passed to loaders and, when the page reads them, serialized into framework navigation/SSR data.
Supported values include:
null- strings
- booleans
- finite numbers
- arrays containing supported values
- plain objects containing supported values
Unsupported values include functions, symbols, BigInt, non-finite numbers, class instances, Date/Map/Set objects and circular references.
Do not place passwords, raw authentication secrets, private keys or other sensitive server-only secrets in guard data. Guard data may be serialized to the browser when the page uses the route framework data pipeline.
Cookies and sessions
Guards run inside the normal BCP request context, so they can use:
import {
cookies,
getSession,
headers,
requestId,
requestMethod,
requestUrl,
} from "bcp/server";Cookies set while an allowed guard runs are preserved on the final document or SPA navigation response. Cookies set before a guard redirect are preserved on that redirect as well.
Direct requests and SPA navigation
The same guard rules apply to:
- direct browser requests
<Link>navigationnavigate()router.push()router.replace()- browser history navigation
- loader-backed SPA navigation
For SPA navigation, the guard evaluates the target page URL rather than the internal /_bcp/navigation transport URL.
A guard redirect is converted to a navigation redirect payload when appropriate so same-origin navigation can continue through the BCP router while preserving response cookies.
Middleware and guard order
Production request order is intentionally layered:
Security gateway
↓
Response cache gateway
↓
Project middleware
↓
Route guard
↓
Page loader
↓
SSR / navigation payloadProject middleware therefore remains the outer application interception layer. Route guards are intended for page authorization and route-specific access policy.
Cache safety
Pages protected by route guards are excluded from the automatic production page response cache. Guard results may depend on sessions, cookies, roles or user-specific state, so caching a guarded document without a user-aware key would be unsafe.
Applications can still use explicit server data caching where the cache key and invalidation strategy are safe for the data being cached.
Internal transport hardening
Standalone production transports evaluated guard data to the inner page runtime through a private framework header. Incoming user-supplied values for that header are removed before guard evaluation and before proxying so a browser cannot forge a successful guard result.
The serialized internal guard payload is size-limited. Applications should return only the small identity/authorization data required by descendant guards, loaders and pages.
Guard versus middleware
Use a route guard when access policy belongs to a page subtree and should integrate directly with loaders and page data.
Use middleware when a request-wide concern needs to run before route execution, such as rewrites, global redirects, shared request policy or non-page request interception.
Both can be used together; middleware runs before route guards.