Skip to content

Umair-N/permx

Repository files navigation

PermX

npm version npm version npm version TypeScript License: MIT Zero Dependencies

Structured RBAC for Node.js and React. Permission keys with meaning, role inheritance that handles diamonds and cycles, UI-aware mappings, multi-tenant support, and a headless React SDK — works with any HTTP framework, and any database (MongoDB via Mongoose, PostgreSQL / MySQL / SQLite / SQL Server via Prisma).

npm install @permx/core                        # backend (zero deps)
npm install @permx/react                       # frontend — React SDK
npm install @permx/prisma @prisma/client       # Prisma adapter (PostgreSQL, MySQL, SQLite)
# or:
npm install @permx/core mongoose               # Mongoose adapter (MongoDB)

The Problem

Most RBAC libraries give you flat string permissions ("read:users") or abstract policy languages. When your app grows, you end up with:

  • Hundreds of unstructured permission strings with no consistent naming
  • Authorization logic scattered across middleware, controllers, and frontend code
  • No connection between backend permissions and what the UI should show or hide
  • SaaS subscription tiers bolted on as a separate system from role permissions
  • Middleware locked to Express, unusable with Fastify, Hono, or Next.js

PermX solves these with structured permission keys (module.resource:field.action.scope), UI mappings baked into each permission, a three-layer model (roles + subscriptions + feature flags), and a framework-agnostic core with zero dependencies.

How It Compares

Capability CASL Casbin Permit.io PermX
Structured keys (module.resource:field.action.scope) No No No Yes
UI mappings (routes/components/fields) No No No Yes
3-layer model (regular + subscription + flags) No No Partial Yes
Role inheritance with DFS + cycle protection No Policy Managed Yes
Framework-agnostic (Express, Hono, Fastify, Koa) Express Yes SaaS Yes
DB-agnostic with adapter pattern No Yes SaaS Yes
React SDK (components, hooks, zero-dep store) <Can> only No No Full suite

Quick Start (5 minutes)

1. Install

npm install @permx/core mongoose express

2. Initialize

import mongoose from 'mongoose';
import { createPermX } from '@permx/core/mongoose';

await mongoose.connect('mongodb://localhost:27017/myapp');

const permx = createPermX({
  connection: mongoose.connection,
});

// Create collections and indexes
await permx.migrate();

3. Seed your first role

Before authorize() can return true, you need a module, a permission, a role, and a user-role assignment. PermX ships a declarative, idempotent seeder — safe to run on every app boot:

import { syncFromConfig } from '@permx/core/mongoose';

await syncFromConfig(permx.models, {
  modules: [
    { name: 'Projects', slug: 'projects' },
  ],
  permissions: [
    { moduleSlug: 'projects', name: 'View Tasks',  key: 'projects.tasks.view.all' },
    { moduleSlug: 'projects', name: 'Create Tasks', key: 'projects.tasks.create.all' },
  ],
  roles: [
    {
      name: 'Project Viewer',
      slug: 'project-viewer',
      permissionKeys: ['projects.tasks.view.all'],
    },
    {
      name: 'Project Editor',
      slug: 'project-editor',
      permissionKeys: ['projects.tasks.create.all'],
      inheritsFrom: ['project-viewer'], // inherits view permissions too
    },
  ],
});

// Assign the role to a user (also idempotent)
import { ensureUserRole } from '@permx/core/mongoose';
const role = await permx.models.Role.findOne({ slug: 'project-editor' });
await ensureUserRole(permx.models, userId, role._id.toString());

All helpers are upsert-based and cross-reference by slug / key — no id wiring needed. For one-off operations use upsertModule, upsertPermission, upsertRole, and ensureUserRole individually.

4. Protect Routes

import { createPermXMiddleware } from '@permx/core/express';

const auth = createPermXMiddleware(permx, {
  extractUserId: (req) => req.user?.id,
});

app.get('/projects', auth.authorize('projects.tasks.view.all'), listProjects);
app.post('/projects', auth.authorize('projects.tasks.create.all'), createProject);
app.delete('/users/:id', auth.authorize('admin.users.delete.all'), deleteUser);

5. Check Permissions

// Direct check
const result = await permx.authorize(userId, 'projects.tasks.view.all');
// → { authorized: true }

// Get all effective permissions (for frontend)
const perms = await permx.getUserPermissions(userId);
// → { permissions: [...], ui_mappings: { routes, components, fields }, modules }

6. Expose the permissions endpoint

PermX is auth-agnostic — it only needs a userId. Bring your own JWT / session / cookie layer; plug the user ID into one route:

// backend: Express route that the React SDK calls
app.get('/api/permissions/my', async (req, res) => {
  try {
    const perms = await permx.getUserPermissions(req.user.id, {
      tenantId: req.headers['x-tenant-id'] as string | undefined,
    });
    res.json(perms); // { permissions, ui_mappings, modules }
  } catch (error) {
    res.status(500).json({ error: 'Failed to load permissions' });
  }
});

That's it — every frontend gate below consumes this single endpoint.

7. Gate the UI with @permx/react

npm install @permx/react

Wrap your app once, then gate components, fields, and routes declaratively:

import { PermXProvider, Can, CanField, RouteGuard, FeatureGate } from '@permx/react';

function App() {
  return (
    <PermXProvider
      fetchPermissions={() => fetch('/api/permissions/my').then((r) => r.json())}
      superAdmin={user.role === 'super-admin'}
      fallback={<Spinner />}
    >
      {/* Component-level gate */}
      <Can componentId="edit-project-btn">
        <EditButton />
      </Can>

      {/* Field-level gate (hides form field) */}
      <CanField fieldId="salary">
        <SalaryInput />
      </CanField>

      {/* Route-level gate (pair with router redirect) */}
      <RouteGuard routeId="/admin" fallback={<NoAccess />}>
        <AdminPage />
      </RouteGuard>

      {/* Feature gate with upgrade-prompt overlay */}
      <FeatureGate
        permission="subscription.sso"
        renderOverlay={() => <UpgradeBanner plan="pro" />}
      >
        <SSOSettings />
      </FeatureGate>
    </PermXProvider>
  );
}

Or use hooks for programmatic checks:

import { useHasPermission, useHasRoute, usePermXReady } from '@permx/react';

function Toolbar() {
  const ready = usePermXReady();
  const canEdit = useHasPermission('projects.tasks.update.own');
  const canViewAdmin = useHasRoute('/admin');

  if (!ready) return <Spinner />;
  return (
    <nav>
      {canEdit && <EditButton />}
      {canViewAdmin && <a href="/admin">Admin</a>}
    </nav>
  );
}

The React SDK is ~5 KB, has zero runtime dependencies (built on React's useSyncExternalStore), works with React 18 and 19, and is router-agnostic. See packages/react/README.md for the full API reference.

Core Concepts

Structured Permission Keys

Permissions follow the format: {module}.{resource}:{field}.{action}.{scope}

import { buildDerivedKey, parsePermissionKey } from '@permx/core';

// Field-level access control: only certain roles can see revenue data
buildDerivedKey({
  module: 'analytics',
  resource: 'reports',
  action: 'view',
  scope: 'own',
  field: 'revenue',
});
// → "analytics.reports:revenue.view.own"

parsePermissionKey('analytics.reports:revenue.view.own');
// → { module: 'analytics', resource: 'reports', field: 'revenue', action: 'view', scope: 'own' }

Actions: view, create, update, delete, manage Scopes: all, own, team, department, self, public, admin

Typed Permission Keys (TypeScript)

Define permissions once and get autocomplete + literal types everywhere — no magic strings:

import { definePermissions, type PermissionKeyOf } from '@permx/core';

export const P = definePermissions({
  projectsView:  { module: 'projects', resource: 'tasks', action: 'view',   scope: 'all' },
  projectsEdit:  { module: 'projects', resource: 'tasks', action: 'update', scope: 'own' },
  viewSalary:    { module: 'people',   resource: 'employees', action: 'view', scope: 'own', field: 'salary' },
} as const);

// Inferred as the literal string "projects.tasks.view.all"
P.projectsView;

// A union of every key you defined — use it to type middleware args
export type AppPermission = PermissionKeyOf<typeof P>;

await permx.authorize(userId, P.projectsView); // fully typed, autocomplete works
app.get('/tasks', auth.authorize(P.projectsView), listTasks);

Benefits: refactor a permission in one place, get a compile error everywhere it was used; IDE autocomplete across the app.

Role Inheritance

Roles can inherit permissions from parent roles. PermX uses DFS with diamond and cycle protection:

Owner (full access)
  └── Admin (inherits Owner's management permissions)
        ├── Editor (inherits Admin — can edit content)
        └── Billing Manager (inherits Admin — can manage payments)
              └── Viewer (inherits from both — diamond handled)

UI Mappings

Each permission can map to routes, components, and fields — enabling frontend gating without hardcoding access rules in your UI:

// Permission "billing.invoices.view.all" maps to:
{
  ui_mappings: [
    { type: 'route', identifier: '/billing' },
    { type: 'component', identifier: 'invoice-table' },
    { type: 'field', identifier: 'payment-amount' },
  ]
}

// Frontend receives pre-computed arrays:
const perms = await permx.getUserPermissions(userId);
perms.ui_mappings.routes;     // ['/billing', '/dashboard', '/settings']
perms.ui_mappings.components; // ['invoice-table', 'export-btn', 'user-list']
perms.ui_mappings.fields;     // ['payment-amount', 'api-key', 'revenue']

Three-Layer Permission Model

For SaaS apps, permissions come from three independent sources:

Effective = Regular Roles ∪ Subscription Roles ∪ Feature Flags

Regular Roles:    Job-function access (per-user assignment)
Subscription:     Tenant plan features (per-tenant, shared by all users)
Feature Flags:    Gradual rollout capabilities (per-tenant)

Example: A user with the "Editor" role on the "Pro" plan gets:

  • Editor permissions (create/update posts, manage media)
  • Pro plan features (analytics dashboard, API access, custom domains)
  • Feature flags (beta AI assistant, new editor UI)

Wiring to Stripe (or any billing provider):

// 1. Create a "subscription" role per plan tier (idempotent seed)
await syncFromConfig(permx.models, {
  roles: [
    { name: 'Free Plan',  slug: 'plan-free',  role_type: 'subscription',
      permissionKeys: ['posts.content.view.all'] },
    { name: 'Pro Plan',   slug: 'plan-pro',   role_type: 'subscription',
      permissionKeys: ['posts.content.view.all', 'analytics.dashboards.view.all'] },
  ],
});

// 2. Tell PermX how to resolve a tenant's active plan at request time
const permx = createPermX({
  connection: mongoose.connection,
  subscriptionResolver: async (tenantId) => {
    const tenant = await TenantModel.findById(tenantId).lean();
    const planRole = await permx.models.Role.findOne({ slug: tenant.planSlug }).lean();
    return planRole ? [planRole._id.toString()] : [];
  },
});

// 3. When Stripe sends a webhook (checkout.session.completed, customer.subscription.updated),
//    just update the tenant's planSlug — PermX picks it up on the next request.
await TenantModel.updateOne({ _id: tenantId }, { $set: { planSlug: 'plan-pro' } });

The resolver is called per authorization; wrap it with the built-in cache (cache: { ttl: 15_000 }) to avoid per-request DB hits.

Entry Points

Import Purpose Dependencies
@permx/core Core types, engine, utilities Zero
@permx/core/mongoose MongoDB adapter + schema factory mongoose
@permx/core/express Express middleware express
@permx/prisma Prisma adapter (PostgreSQL, MySQL, SQLite, SQL Server) @prisma/client (peer)
@permx/react React components, hooks, and zero-dep store react (peer)

@permx/core — Core (Zero Dependencies)

import {
  // Key utilities
  buildDerivedKey,
  parsePermissionKey,
  definePermissions,
  type PermissionKeyOf,

  // Engine (for custom adapters)
  resolveRolePermissions,
  detectCircularInheritance,
  matchPathPattern,
  createPermXCore,

  // Framework-agnostic authorization
  handleAuthorization,
  handleApiAuthorization,
  type AuthorizationRequest,
  type AuthorizationOutcome,

  // Cache
  TtlCache,

  // Errors
  PermXError,
  PermissionDeniedError,
  CircularInheritanceError,

  // Types
  type Permission,
  type Role,
  type Module,
  type UserRole,
  type PermXDataProvider,
  type PermXConfig,
  type EffectivePermissions,
  type AuthResult,

  // Constants
  PERMISSION_ACTIONS,
  PERMISSION_SCOPES,
  ROLE_TYPES,
} from '@permx/core';

@permx/core/mongoose — MongoDB Adapter

import {
  createPermX,
  createPermXSchemas,
  MongooseDataProvider,
  tenantPlugin,

  // Idempotent seed helpers (safe to run on every boot)
  syncFromConfig,
  upsertModule,
  upsertPermission,
  upsertRole,
  ensureUserRole,

  type MongoosePermXConfig,
  type MongoosePermXInstance,
  type SchemaFactoryConfig,
  type PermXModels,
} from '@permx/core/mongoose';

@permx/prisma — Prisma Adapter (peer: @prisma/client)

Drop-in adapter for PostgreSQL, MySQL, SQLite, SQL Server, or MongoDB via Prisma. Copy the models from the shipped schema.prisma, run prisma migrate, and wire it up in 3 lines:

import { PrismaClient } from '@prisma/client';
import { createPermX, syncFromConfig } from '@permx/prisma';

const prisma = new PrismaClient();

const permx = createPermX({
  prisma,
  cache: { ttl: 15_000 },
  tenancy: { enabled: true },
  superAdmin: { check: (userId) => userId === 'admin' },
});

await syncFromConfig(prisma, {
  modules: [{ name: 'Projects', slug: 'projects' }],
  permissions: [
    { moduleSlug: 'projects', name: 'View', key: 'projects.tasks.view.all' },
  ],
  roles: [{ name: 'Viewer', slug: 'viewer', permissionKeys: ['projects.tasks.view.all'] }],
});

await permx.authorize(userId, 'projects.tasks.view.all');

Full exports (PrismaDataProvider, syncFromConfig, upsertModule, upsertPermission, upsertRole, ensureUserRole, setRolePermissions, setRoleInheritance, PrismaClientLike) — see packages/prisma/README.md.

@permx/core/express — Middleware

import {
  createPermXMiddleware,
  type PermXMiddleware,
  type PermXMiddlewareConfig,
} from '@permx/core/express';

@permx/react — React SDK (peer: react)

import {
  // Provider
  PermXProvider,

  // Gate components (headless — no CSS bundled)
  Can,
  CanField,
  RouteGuard,
  FeatureGate,

  // Hooks
  useHasPermission,
  useHasRoute,
  useHasComponent,
  useHasField,
  usePermissions,
  usePermXReady,

  // Advanced (custom providers, testing, SSR hydration)
  createPermissionStore,
  PermissionStore,

  // Types
  type PermXProviderProps,
  type CanProps,
  type CanFieldProps,
  type RouteGuardProps,
  type FeatureGateProps,
  type PermissionState,
} from '@permx/react';

Configuration

Full Configuration Example

import { createPermX } from '@permx/core/mongoose';

const permx = createPermX({
  // Required: your Mongoose connection
  connection: mongoose.connection,

  // Optional: rename collections (default: PermX_Module, PermX_Permission, etc.)
  collections: {
    module: 'acl_modules',
    permission: 'acl_permissions',
    role: 'acl_roles',
    userRole: 'acl_user_roles',
  },

  // Optional: extend schemas with custom fields
  extend: {
    role: { department: { type: String } },
    userRole: { notes: { type: String } },
  },

  // Optional: multi-tenancy
  tenancy: {
    enabled: true,
    tenantIdField: 'tenantId',                  // default
    exemptModels: ['module', 'permission'],      // global (not per-tenant)
  },

  // Optional: subscription-based permissions (SaaS)
  subscriptionResolver: async (tenantId) => {
    const tenant = await TenantModel.findById(tenantId);
    return tenant?.planPermissionIds ?? [];
  },

  // Optional: super-admin bypass
  superAdmin: {
    check: (userId) => userId === 'admin-user-id',
  },

  // Optional: API permission map cache
  cache: { ttl: 15_000 },
});

Express Middleware Configuration

import { createPermXMiddleware } from '@permx/core/express';

const auth = createPermXMiddleware(permx, {
  // Required: how to get user ID from the request
  extractUserId: (req) => req.user?.id,

  // Optional: tenant context for multi-tenant apps
  extractTenantId: (req) => req.headers['x-tenant-id'] as string,

  // Optional: service-to-service bypass
  isServiceCall: (req) => req.headers['x-api-key'] === process.env.SERVICE_KEY,

  // Optional: super-admin bypass at middleware level
  isSuperAdmin: (req) => req.user?.role === 'super-admin',

  // Optional: custom denied response
  onDenied: (req, res, permissionKey) => {
    res.status(403).json({
      error: 'Forbidden',
      required_permission: permissionKey,
    });
  },

  // Optional: custom error response
  onError: (req, res, error) => {
    res.status(500).json({ error: 'Authorization service unavailable' });
  },
});

// Per-route authorization
router.get('/projects', auth.authorize('projects.tasks.view.all'), handler);
router.post('/billing/invoices', auth.authorize('billing.invoices.create.all'), handler);

// Gateway-style API mapping authorization
router.use(auth.authorizeApi('project-service'));

Framework-Agnostic Authorization

The core package exports handleAuthorization and handleApiAuthorization — pure async functions that work with any HTTP framework (Hono, Fastify, Koa, Next.js, etc.):

import {
  handleAuthorization,
  handleApiAuthorization,
  type AuthorizationRequest,
  type AuthorizationOutcome,
} from '@permx/core';

// 1. Map your framework's request to AuthorizationRequest
const request: AuthorizationRequest = {
  userId: getUserIdFromYourFramework(),
  tenantId: getTenantIdFromYourFramework(),
  isServiceCall: false,
  isSuperAdmin: false,
};

// 2. Call the handler
const outcome = await handleAuthorization(permx, request, 'projects.tasks.view.all');

// 3. Map the outcome to your framework's response
if (outcome.action === 'allow')  { /* next() */ }
if (outcome.action === 'deny')   { /* 403 response */ }
if (outcome.action === 'error')  { /* 500 response */ }

The Express middleware (@permx/core/express) is a thin wrapper around these functions. See examples/hono-adapter.ts for a complete Hono adapter in ~20 lines.

Next.js App Router

PermX runs in Next.js Route Handlers, Server Components, and Middleware. Full reference: examples/nextjs-app-router.ts.

Route Handler (app/api/permissions/my/route.ts) — the endpoint the React SDK calls:

import { NextResponse } from 'next/server';
import { permx } from '@/lib/permx';
import { getUserIdFromSession } from '@/lib/auth';

export async function GET(req: Request) {
  const userId = await getUserIdFromSession(req);
  if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

  const perms = await permx.getUserPermissions(userId);
  return NextResponse.json(perms);
}

Server Component — gate pages server-side (no client-side flash):

// app/admin/page.tsx
import { redirect } from 'next/navigation';
import { permx } from '@/lib/permx';
import { getUserIdFromSession } from '@/lib/auth';

export default async function AdminPage() {
  const userId = await getUserIdFromSession();
  const { authorized } = await permx.authorize(userId, 'admin.users.view.all');
  if (!authorized) redirect('/403');

  return <AdminDashboard />;
}

Middleware — block unauthorized routes before the page renders:

// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
import { handleAuthorization } from '@permx/core';
import { permx } from '@/lib/permx';

export async function middleware(req: NextRequest) {
  const userId = req.cookies.get('userId')?.value;
  if (!userId) return NextResponse.redirect(new URL('/login', req.url));

  const outcome = await handleAuthorization(permx, { userId }, 'admin.dashboard.view.all');
  if (outcome.action === 'deny') return NextResponse.redirect(new URL('/403', req.url));

  return NextResponse.next();
}

export const config = { matcher: ['/admin/:path*'] };

Client Component@permx/react works unchanged inside a "use client" boundary; just point fetchPermissions at the Route Handler above.

Cache Invalidation

PermX caches effective permissions per user (and per tenant) for the configured TTL. When an admin changes a user's role, call the invalidation API so the next authorize() sees fresh data — don't wait for the TTL:

const permx = createPermX({
  connection: mongoose.connection,
  cache: { ttl: 60_000 }, // 60-second TTL
});

// Admin changes a user's role:
await ensureUserRole(permx.models, targetUserId, newRoleId);
permx.invalidateUser(targetUserId);                              // clear that user's cache
// or with multi-tenancy:
permx.invalidateUser(targetUserId, { tenantId: 'acme' });

// Bulk permission / role schema change:
permx.invalidateAll();                                           // nuke user + api-map caches

Telling the frontend to refetch — the React SDK caches the /api/permissions/my response. After a role change, either:

  1. Call usePermXRefresh() and invoke the returned refresh() function, or
  2. Reload the page / re-mount <PermXProvider>, or
  3. Push an SSE / WebSocket event from your backend after invalidateUser() and trigger refresh() from the listener.
import { usePermXRefresh, usePermXError } from '@permx/react';

function RoleEditor({ userId }: { userId: string }) {
  const refresh = usePermXRefresh();
  const error = usePermXError();

  async function onSave(roleId: string) {
    await fetch(`/api/users/${userId}/roles`, { method: 'POST', body: JSON.stringify({ roleId }) });
    await refresh(); // pick up new permissions without reload
  }

  return <>{error && <ErrorBanner error={error} onRetry={refresh} />}...</>;
}

For multi-instance deployments, pair invalidateUser() with a pub/sub broadcast (Redis, NATS) so every app node clears its local cache.

Observability

PermX emits lifecycle events via a built-in EventEmitter. Subscribe to pipe them into your logging, metrics, or audit system:

permx.emitter?.on('authorize', ({ userId, permissionKey, authorized, duration_ms }) => {
  metrics.histogram('permx.authorize.duration', duration_ms, { outcome: authorized ? 'allow' : 'deny' });
});

permx.emitter?.on('authorize.denied', ({ userId, permissionKey }) => {
  auditLog.write({ kind: 'authz_denied', userId, permissionKey, at: new Date() });
});

permx.emitter?.on('cache.hit', ({ key }) => metrics.counter('permx.cache.hit').inc({ key }));
permx.emitter?.on('cache.miss', ({ key }) => metrics.counter('permx.cache.miss').inc({ key }));

Available events: authorize, authorize.denied, authorize.error, cache.hit, cache.miss. Listeners run asynchronously via queueMicrotask — throwing inside a listener never breaks the authorization path.

Dev-Mode Typo Warnings

Pass your known permission keys to dev.knownKeys and PermX will log a one-time warning any time authorize() is called with a key that isn't in the set — catching typos before they become silent denials.

import { definePermissions } from '@permx/core';

export const P = definePermissions({
  viewTasks: { module: 'projects', resource: 'tasks', action: 'view', scope: 'all' },
} as const);

const permx = createPermX({
  connection: mongoose.connection,
  dev: process.env.NODE_ENV !== 'production'
    ? { knownKeys: Object.values(P) }
    : undefined,
});

// If your code accidentally calls:
await permx.authorize(userId, 'projects.tasks.view.typo');
// → console.warn: [permx] authorize() called with unknown permission key 'projects.tasks.view.typo'

Warnings are deduplicated per key. Omit dev.knownKeys in production for zero overhead.

Coming From CASL / Casbin

Task CASL Casbin PermX
Define a permission defineAbility((can) => can('read', 'Post')) policy CSV: p, alice, data, read definePermissions({ viewPost: { module: 'posts', resource: 'items', action: 'view', scope: 'all' }})
Check a permission ability.can('read', 'Post') enforcer.enforce(user, 'data', 'read') permx.authorize(userId, 'posts.items.view.all')
Field-level access custom field conditions policy + matcher module.resource:field.action.scope — built in
Multi-tenancy DIY DIY tenancy: { enabled: true }
Subscription tiers DIY DIY subscriptionResolver(tenantId)
Gate a React component <Can I="read" a="Post"> none <Can componentId="post-editor">
Gate a route DIY DIY <RouteGuard routeId="/admin">

The mental shift: CASL encodes subject + verb (read Post); PermX encodes location in the app (posts.items.view.all) so UI mappings and role inheritance work uniformly across backend and frontend.

Building Custom Data Adapters

PermX's core engine is database-agnostic. To use a different database, implement the PermXDataProvider interface:

import { createPermXCore, type PermXDataProvider } from '@permx/core';

class PrismaDataProvider implements PermXDataProvider {
  async getUserRoles(userId: string) { /* Prisma queries */ }
  async getRoleForResolution(roleId: string) { /* Prisma queries */ }
  async getPermissionsByIds(ids: string[]) { /* Prisma queries */ }
  async getModulesByIds(ids: string[]) { /* Prisma queries */ }
  async getApiPermissionMap() { /* Prisma queries */ }
}

const permx = createPermXCore(new PrismaDataProvider(), {
  cache: { ttl: 15_000 },
  superAdmin: { check: (userId) => userId === 'admin' },
});

Architecture

This is a monorepo with two published packages:

packages/core/          → @permx/core (zero deps)
├── types/              8 type definition files
├── engine/             Permission key parser, DFS resolver, circular detector, path matcher
├── middleware/
│   └── handler.ts      Framework-agnostic handleAuthorization + handleApiAuthorization
├── cache/              Generic TTL cache
├── errors.ts           Error class hierarchy
├── permx.ts            createPermXCore() factory
├── mongoose/           (peer: mongoose) Schema factory, data provider, tenant plugin
└── middleware/         (peer: express)  Thin Express wrapper over handler.ts

packages/react/         → @permx/react (peer: react, zero runtime deps)
├── store.ts            PermissionStore class with useSyncExternalStore-compatible API
├── context.ts          React context + internal usePermXStore hook
├── provider.tsx        <PermXProvider> — fetches on mount, hydrates store
├── components/         <Can>, <CanField>, <RouteGuard>, <FeatureGate> (headless)
└── hooks/              useHasPermission, useHasRoute, useHasComponent, useHasField,
                        usePermissions, usePermXReady

Development

# Install dependencies for the whole workspace
bun install

# Run tests for all packages (261 tests total)
bun run test

# Run just one package
bun run test:core      # @permx/core   — 204 tests
bun run test:react     # @permx/react  — 57 tests

# Build all packages (dual CJS/ESM)
bun run build

# Type check and lint the whole monorepo
bun run typecheck
bun run lint

License

MIT

About

No description, website, or topics provided.

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors