npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

aegis-guard

v0.3.0

Published

Dynamic, database driven RBAC/ABAC for Next.js App Router. Shield your Server Components at runtime.

Readme

aegis-guard

Dynamic, database driven RBAC/ABAC for Next.js App Router. Shield your Server Components at runtime.

  • Server first: gates content in React Server Components before it reaches the client
  • Database agnostic: bring your own adapter (Prisma, Drizzle, raw SQL, anything)
  • Auto discovery: <Shield> registers resources in your database during development
  • Admin dashboard: built in UI for managing permissions (headless, unstyled, or styled)

Install

npm install aegis-guard

Peer dependencies: next >= 16, react >= 18, react-dom >= 18

Quick Start

1. Implement an adapter

// lib/aegis-adapter.ts
import type { AegisAdapter } from "aegis-guard/core";

export const adapter: AegisAdapter = {
  getUserRoles: async (ctx) => {
    // Query your database for the user's roles
    return db.getUserRoles(ctx.userId);
  },
  getAllowedRoles: async (resourceKey) => {
    return db.getResourceRoles(resourceKey);
  },
  getAllResources: async () => {
    return db.getAllResources();
  },
  setResourcePermissions: async (resourceKey, roles) => {
    await db.updateResourceRoles(resourceKey, roles);
  },
  upsertResource: async (resourceKey, description) => {
    await db.upsertResource(resourceKey, description);
  },
};

2. Initialize Aegis

Use Next.js instrumentation to initialize once at server startup:

// instrumentation.ts
export async function register() {
  const { initAegis } = await import("aegis-guard/core");
  const { adapter } = await import("./lib/aegis-adapter");

  initAegis({
    adapter,
    getSecurityContext: async () => {
      const { auth } = await import("./lib/auth"); // your auth library
      const session = await auth();
      return { userId: session?.userId ?? null };
    },
  });
}

3. Protect pages with Shield

// app/admin/page.tsx
import { Shield } from "aegis-guard/server";

export default function AdminPage() {
  return (
    <Shield
      resource="admin.page"
      description="Main admin page"
      fallback={<p>Access denied.</p>}
    >
      <h1>Welcome, admin</h1>
    </Shield>
  );
}

In development, <Shield> automatically calls adapter.upsertResource() to register the resource in your database.

4. Protect API routes and middleware

// app/api/secret/route.ts
import { checkPermission } from "aegis-guard/server";

export async function GET() {
  const allowed = await checkPermission("api.secret");
  if (!allowed) {
    return Response.json({ error: "Forbidden" }, { status: 403 });
  }
  return Response.json({ data: "secret" });
}

5. Add the admin API route

// app/api/aegis/route.ts
import { createAegisHandler } from "aegis-guard/server";

export const { GET, POST } = createAegisHandler();

6. Add the permissions dashboard

// app/admin/permissions/page.tsx
import { AegisDashboardStyled } from "aegis-guard/client";

export default function PermissionsPage() {
  return <AegisDashboardStyled roles={["admin", "editor", "viewer"]} />;
}

7. Gate routes with middleware (optional)

createAegisMiddleware maps path patterns to resources and gates them before the request reaches your pages.

// middleware.ts
import { createAegisMiddleware } from "aegis-guard/server";

export const middleware = createAegisMiddleware({
  rules: [
    { matcher: "/admin/*", resource: "admin.area" },
    { matcher: "/billing", resource: "billing.view" },
    { matcher: /^\/reports\/\d+$/, resource: "reports.view" },
  ],
  redirectTo: "/login", // omit to return 403, or pass `onDenied` for full control
});

export const config = {
  matcher: ["/admin/:path*", "/billing", "/reports/:id"],
};

Matchers can be an exact path ("/billing"), a prefix with a trailing /* ("/admin/*"), or a RegExp. The first matching rule wins; unmatched paths pass through.

Runtime note: Next.js middleware runs on the Edge runtime by default, where the config set by initAegis() in instrumentation.ts (Node runtime) is not visible. To use this helper, either opt your middleware into the Node.js runtime, or call initAegis() with an Edge compatible adapter inside the middleware module.

API Reference

aegis-guard/core

| Export | Description | | --------------------------------- | ----------------------------------------------------------------- | | initAegis(config) | Initialize the library with your adapter and auth context | | createGuard(config) | Build a portable AegisGuard. The base for any framework binding | | evaluatePermission(r,c,d) | Pure decision engine. Explicit context and deps, no globals | | AegisAdapter | Interface your database adapter must implement | | AegisConfig | Configuration object type | | AegisContext | User context type ({ userId, ...custom }) | | AegisResource | Resource type ({ key, description? }) | | AegisGuard / BoundGuard | The universal guard surface and its per-request binder | | AegisDecision | Explainable result (allowed, reason, matched/missing roles) | | GuardInput | Context input union (omit / { request } / AegisContext) | | Validator<T> | (data: unknown) => T. Runtime validator type (Zod, Valibot, ...) | | createValidatedAdapter | Wrap an adapter so its returned data is validated | | AegisError / AegisDeniedError | Typed config error and denial error (.decision) | | AegisValidationError | Thrown when a validator rejects (.source, .cause) |

aegis-guard/server

| Export | Description | | -------------------------------- | ------------------------------------------------------------ | | <Shield> | Async Server Component that gates content by permission | | checkPermission(resource) | Returns boolean. Use in API routes, middleware, tRPC | | decidePermission(resource) | Returns the full AegisDecision (allowed, reason, roles) | | requirePermission(resource) | Returns the decision, or throws AegisDeniedError on deny | | createGuard(config) | Re-exported from core for request-scoped or custom guards | | createAegisHandler() | Returns { GET, POST } handlers for the admin API | | createAegisMiddleware(options) | Returns a Next.js middleware that gates routes by permission |

aegis-guard/client

| Export | Description | | -------------------------------- | --------------------------------------------------------- | | useAegisResources(options?) | Hook to fetch resources with their roles | | useUpdatePermissions(options?) | Hook to update resource permissions | | <AegisDashboard> | Unstyled dashboard with semantic HTML. Bring your own CSS | | <AegisDashboardStyled> | Tailwind styled dashboard, ready to use |

Adapter Interface

interface AegisAdapter {
  getUserRoles(ctx: AegisContext): Promise<string[]>;
  getAllowedRoles(resourceKey: string): Promise<string[]>;
  getAllResources(): Promise<AegisResource[]>;
  setResourcePermissions(resourceKey: string, roles: string[]): Promise<void>;
  upsertResource(resourceKey: string, description?: string): Promise<void>;
}

Universal guard (framework agnostic)

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 through cookies() and headers() with no argument.

For request scoped frameworks (Astro, SvelteKit, Express, Hono, Cloudflare Workers, tRPC), 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,
  // A source aware resolver: derive the context from the passed request.
  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.check("admin.page");

// Throw on denial (idiomatic for middleware and loaders):
import { AegisDeniedError } from "aegis-guard/core";
try {
  await guard.require("admin.page", { request });
} catch (error) {
  if (error instanceof AegisDeniedError) {
    // error.decision.reason: "no-matching-role" | "closed-by-default" | ...
  }
}

Every method can return an explainable AegisDecision (via decide / decideMany):

const decision = await guard.decide("admin.page", { request });
// {
//   allowed: false,
//   resource: "admin.page",
//   reason: "no-matching-role",
//   userId: "u_123",
//   allowedRoles: ["admin"],
//   userRoles: ["editor"],
//   matchedRoles: [],            // why allowed
//   missingRoles: ["admin"],     // why denied
// }

The pure engine (evaluatePermission(resource, context, { adapter })) takes the context and adapter explicitly, touches no globals, and runs on any runtime. Framework bindings are thin layers over createGuard and this engine.

Runtime validation (optional)

The adapter returns data from a database that aegis-guard does not control. You can validate that data at runtime with any validation library, or none. A validator is just a function (data: unknown) => T that returns the value or throws. Register validators on initAegis (or createGuard):

import { z } from "zod"; // your dependency, not aegis-guard's

initAegis({
  adapter,
  getSecurityContext,
  validators: {
    context: (d) => z.object({ userId: z.string().nullable() }).strict().parse(d),
    roles: (d) => z.array(z.string()).parse(d),       // getUserRoles + getAllowedRoles
    resource: (d) => z.object({ key: z.string(), description: z.string().optional() }).parse(d),
    body: (d) => z.object({ resource: z.string().min(1), roles: z.array(z.string()) }).parse(d),
  },
});

The same shape works with other libraries, with no aegis-guard dependency on them:

// Valibot
roles: (d) => v.parse(v.array(v.string()), d),
// ArkType
roles: type("string[]").assert,
// Hand written
roles: (d) => {
  if (!Array.isArray(d) || d.some((x) => typeof x !== "string")) {
    throw new TypeError("roles must be string[]");
  }
  return d as string[];
},

Validation fails closed. If a validator throws, the call rejects with AegisValidationError (extends AegisError, code VALIDATION_FAILED) before any decision is scored, so a check can never silently grant access on bad data. A denied request still throws AegisDeniedError, so you can tell a real denial (map to 403) apart from corrupt data (map to 500):

import { AegisDeniedError, AegisValidationError } from "aegis-guard/server";

try {
  await requirePermission("admin.page");
} catch (error) {
  if (error instanceof AegisDeniedError) return new Response("Forbidden", { status: 403 });
  if (error instanceof AegisValidationError) return new Response("Server error", { status: 500 });
  throw error;
}

Notes:

  • Validators are opt in. With none configured, behavior is identical to before and adds no overhead.
  • Adapter validators must only assert, never transform. A validator that adds roles or claims would widen access. Use strict schemas for context so unexpected fields are rejected.
  • Validators are synchronous.

Dashboard Props

Both <AegisDashboard> and <AegisDashboardStyled> accept:

| Prop | Type | Default | Description | | ----------- | ---------- | --------------------- | ------------------------------ | | roles | string[] | required | All possible roles in your app | | apiBase | string | "/api/aegis" | Base path for the admin API | | className | string | | CSS class for the root element | | title | string | "Aegis Permissions" | Dashboard heading |

Hook Options

Both hooks accept { apiBase?: string } (default "/api/aegis").

Tailwind Setup

If using <AegisDashboardStyled>, ensure Tailwind scans the library's classes:

Tailwind v4 add to your CSS:

@source "../../node_modules/aegis-guard/dist";

Tailwind v3 add to tailwind.config.js:

content: [
  "./node_modules/aegis-guard/dist/**/*.{js,mjs}",
  // ...your other content paths
];

Examples

See the apps/playground/ directory for a complete working demo with an in memory adapter, protected pages, role switching, and the permissions dashboard.

See examples/ for adapter implementations:

License

MIT