Docs/Authentication & Authorization/Auth Route Guards

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:

TypeScript
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:

TypeScript
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():

TypeScript
import {
    createGuestGuard,
} from "bcp/auth";

export const guard =
    createGuestGuard({
        redirectTo:
            "/dashboard",
    });

Behavior:

text
anonymous user
    ↓
page continues

signed-in user
    ↓
303 /dashboard

Use redirectTo: null to return 409 Already authenticated instead of redirecting.

The low-level form is requireGuest().

Require a role

TypeScript
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:

TypeScript
export const guard =
    createRoleGuard<User>([
        "admin",
        "manager",
    ]);

Require all values from a role field:

TypeScript
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.

TypeScript
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:

TypeScript
export const guard =
    createPermissionGuard<User>(
        [
            "users.read",
            "users.write",
        ],
        {
            match: "all",
        }
    );

Use another user field such as scopes:

TypeScript
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:

TypeScript
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:

TypeScript
{
    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:

TypeScript
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.