Skip to Content
GuidesUniversal guard

Universal guard

The same permission engine powers Shield, API routes, middleware, and any future framework binding. checkPermission, decidePermission, and requirePermission are convenience wrappers over a global guard built from initAegis. They suit ambient frameworks like Next.js, where the request is available with no argument.

Explainable decisions

decidePermission returns the full AegisDecision, which explains itself:

import { decidePermission } from "aegis-guard/server"; const decision = await decidePermission("admin.settings"); // { // allowed: false, // resource: "admin.settings", // reason: "no-matching-role", // userId: "u_123", // allowedRoles: ["admin"], // userRoles: ["editor"], // matchedRoles: [], // why allowed // missingRoles: ["admin"], // why denied // }

The reason is one of granted, no-matching-role, closed-by-default, or no-context. The matched and missing role lists show exactly which roles drove the outcome.

Try it live

The cards below are real decisions, rendered by decidePermission against the demo adapter for your current role. Open the playground and switch users, then return here: these update too, because the docs and the playground share one Aegis Guard init and the same aegis-user cookie.

Live decisions for your current role

Loading decisions...

The same decision through a route handler that uses requirePermission:

Probe the API guard

Calls a route handler that uses requirePermission. A denial throws AegisDeniedError carrying the decision. Switch users in the header, then re-run.

Throwing variant

requirePermission returns the decision, or throws AegisDeniedError (carrying .decision) when denied. This is idiomatic for route handlers and loaders:

import { requirePermission, AegisDeniedError } from "aegis-guard/server"; export async function GET() { try { await requirePermission("api.secret"); } catch (error) { if (error instanceof AegisDeniedError) { return Response.json({ reason: error.decision.reason }, { status: 403 }); } throw error; } return Response.json({ data: "secret" }); }

Request scoped frameworks

For frameworks without an ambient request (Astro, SvelteKit, Express, Hono, Cloudflare Workers), build a guard with createGuard and pass the request per call. The same AegisAdapter works unchanged across every framework.

import { createGuard } from "aegis-guard/core"; const guard = createGuard({ adapter, resolve: async (request) => ({ userId: await getUserId(request) }), }); // Three ways to supply context to any guard method: await guard.check("admin.page"); // ambient: uses resolve() await guard.check("admin.page", { request }); // request scoped await guard.check("admin.page", { userId: "u_123" }); // pre resolved context // Bind a request once, then call with no extra argument: const bound = guard.forRequest(request); await bound.require("admin.page");

A guard exposes decide, check, require, decideMany, and forRequest. The pure engine, evaluatePermission(resource, context, { adapter }), takes context and adapter explicitly, touches no globals, and runs on any runtime. Framework bindings are thin layers over createGuard and this engine.

Last updated on