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)
src/app/examples/role-gating/page.tsxTSX
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 { ... };
}