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