Role-based access control
A gate (defineGate) declares who can do what; the UI shows sections via gate.for(session).can(…) and loaders re-check server-side with gate.denies(…) then throw forbidden() — the framework catches the throw and renders the 403.
Tip
In real apps, store the role in an HMAC-signed cookie (or a JWT) rather than plain text — an unsigned cookie can be edited by the user in DevTools. The loader must always re-verify the role server-side even if the UI already hides the section.
Current session
user: guest · role: guest
Role-gated content
👋 Public — visible to everyone including guests.
🔒 Members area (log in to view)
⚠ Admin tools (admin role required)
import { defineGate, hasRole, forbidden } from "@bext-stack/framework/authz";
// Declare abilities once. Each receives the user (or null for a guest) plus
// any resource args, so ownership checks live here too. A `before` hook is a
// global short-circuit — the place for a super-admin bypass.
const gate = defineGate<Session>({
"members.view": (u) => hasRole(u, "user", "admin"),
"admin.access": (u) => hasRole(u, "admin"),
"post.update": (u, post) => hasRole(u, "admin") || post.authorId === u?.id,
}, { before: (u) => hasRole(u, "superadmin") ? true : undefined });
// In a component: show/hide UI.
const acl = gate.for(session);
acl.can("admin.access") // boolean
// In a loader/action: re-check server-side and 403 if denied.
export async function loader({ request }) {
const session = readSession(request);
if (gate.denies(session, "admin.access")) throw forbidden("admins only");
return { ... };
}