Authentication & Authorization
Auth Route Guards
Auth Route Guards — Authentication & Authorization documentation for BCP Framework.
Last updated: 2026-08-29T17:13:55
Auth Route Guards
BCP Framework integrates Authentication Platform v2 and Authorization & Security v2 with route guards through the server-only bcp/auth entrypoint.
Protect a route tree
Create guard.ts in the route directory you want to protect:
import {
createAuthGuard,
} from "bcp/auth";
export const guard =
createAuthGuard({
redirectTo:
"/login",
});The guard redirects unauthenticated requests to /login with status 303 by default.
To return 401 Unauthorized instead:
export const guard =
createAuthGuard({
redirectTo:
null,
});Guest-only routes — 0.2.5+
Login, registration, and password-recovery entry pages often should not remain accessible after the user has already authenticated.
Use createGuestGuard():
import {
createGuestGuard,
} from "bcp/auth";
export const guard =
createGuestGuard({
redirectTo:
"/dashboard",
});Behavior:
anonymous user
↓
page continues
signed-in user
↓
303 /dashboardUse redirectTo: null to return 409 Already authenticated instead of redirecting.
The low-level form is requireGuest().
Require a role
import {
createRoleGuard,
} from "bcp/auth";
interface User {
id: number;
email: string;
role: string;
}
export const guard =
createRoleGuard<User>(
"admin",
{
redirectTo:
"/login",
}
);An authenticated user without the required role receives 403 Forbidden by default.
Require one of several roles:
export const guard =
createRoleGuard<User>([
"admin",
"manager",
]);Require all values from a role field:
export const guard =
createRoleGuard<User>(
[
"admin",
"billing",
],
{
match: "all",
}
);Require permissions — 0.2.6+
Authorization & Security v2 adds a dedicated permission guard instead of requiring applications to overload role configuration.
import {
createPermissionGuard,
} from "bcp/auth";
interface User {
id: number;
permissions: string[];
}
export const guard =
createPermissionGuard<User>(
"users.read"
);The default permission field is permissions.
Require every permission:
export const guard =
createPermissionGuard<User>(
[
"users.read",
"users.write",
],
{
match: "all",
}
);Use another user field such as scopes:
export const guard =
createPermissionGuard<User>(
"billing.read",
{
permissionField:
"scopes",
}
);The low-level form is requirePermission().
Authenticated users without the required permission receive 403 Forbidden by default. Use forbiddenRedirectTo when the application prefers a redirect.
For authorization decisions that depend on a resource or ownership rather than a flat permission, use the policy APIs documented in Authorization & Security v2.
Inline guard logic
Use requireAuth() when a guard needs additional application-specific checks:
import {
requireAuth,
} from "bcp/auth";
export async function guard() {
const result =
await requireAuth<AppUser>();
if (
result instanceof Response
) {
return result;
}
if (
!result.auth.user.active
) {
return new Response(
"Forbidden",
{
status: 403,
}
);
}
return result;
}Session-store policies apply inside guards
requireAuth(), requireRole(), requirePermission(), and requireGuest() all use the same auth() runtime.
When auth options include a server-side store and idle timeout, guard evaluation observes revocation and inactivity before allowing the route.
For shared application configuration, create a small app auth module and expose guard helpers that use the same store instead of constructing unrelated stores inside each route file.
Auth data in child guards and loaders
Successful auth helpers return guard data in this shape:
{
auth: {
sid,
user,
data,
iat,
exp,
iss,
aud,
},
}BCP merges parent guard data into child guards and page loaders, so descendants can reuse the authenticated session.
Use getGuardAuth() for typed access:
import {
getGuardAuth,
} from "bcp/auth";
export async function loader({
guardData,
}) {
const session =
getGuardAuth<AppUser>(
guardData
);
if (!session) {
throw new Error(
"Protected loader did not receive auth guard data."
);
}
return {
userId:
session.user.id,
};
}Standalone production behavior
bcp/auth stays inside the production guard/runtime request-context graph. requireAuth(), requireGuest(), requireRole(), requirePermission(), auth(), getSession(), and cookies() therefore observe the active request after bcp build.
The production form-action bundle uses the same runtime boundary because guards may execute before an action.
Security boundary
bcp/auth is server-only. BCP blocks it from page/client graphs and maps its browser export to the server-only runtime guard.
Authentication and authorization checks should stay in guards, loaders, actions, API routes, or other server modules.