Skip to content

Mostafashadow1/accessly

Repository files navigation

Accessly - Explainable access control for React

Accessly

Explainable access control for React and Next.js.

Website · Docs · Lab · npm · Bundle size

npm version bundle size dependencies tree shaking TypeScript license

Accessly is a small React permission layer for rendering UI from a normalized access model. It provides permission components, hooks, backend adapters, navigation filtering, feature flag checks, RBAC expansion, wildcard matching, and explainable allow/deny decisions.

This repository contains the published accessly package and the public website/docs/Lab.

Accessly is frontend access control. It controls what React renders for the current authenticated user; it does not replace backend authorization.

Why Accessly?

Frontend access logic often starts as simple conditionals and grows into scattered role checks, permission strings, feature flag branches, and duplicated navigation rules.

Accessly gives React apps one consistent way to ask access questions:

  • Can this user see this UI?
  • Which permission matched?
  • Was access granted directly, through a role, by wildcard, or by feature flag?
  • Which permission is missing?
  • Can this backend response be normalized without changing the backend?

Package Features

  • PermissionProvider for supplying access data to React.
  • Can, Cannot, ProtectedRoute for declarative UI gating.
  • usePermission for boolean permission checks.
  • useAccessDecision for inspectable allow/deny decisions.
  • useAccessModel for reading the normalized model.
  • RBAC expansion with rolePermissions.
  • Wildcard permissions such as users.*, reports.*, and *.
  • Feature flag checks with { flag: "features.new-dashboard" }.
  • Backend adapters with createAdapter.
  • Built-in adapters for flat permissions, grouped actions, pages, nested modules, and feature flags.
  • Navigation filtering with nested menu support.
  • Debug utilities for formatting decisions and inspecting access models.
  • TypeScript declarations for ESM and CJS consumers.
  • Zero runtime dependencies: no regular dependencies; React is a peer dependency.
  • Tree-shaking friendly: ESM build, package exports, and "sideEffects": false.

Installation

npm install accessly

Other package managers:

pnpm add accessly
yarn add accessly
bun add accessly

Quick Start

import { PermissionProvider, Can } from "accessly";

export function App() {
  return (
    <PermissionProvider
      access={{
        user: { id: "user_1", roles: ["admin"] },
        permissions: ["users.create", "reports.view"],
        flags: ["features.new-dashboard"],
      }}
    >
      <Can permission="users.create" fallback={<span>Read only</span>}>
        <button>Create user</button>
      </Can>
    </PermissionProvider>
  );
}

Important Model Rules

  • AccessModel is for the current authenticated user/session only.
  • Do not pass every user in your system to PermissionProvider.
  • PermissionProvider stores access data in React Context.
  • usePermission reads from PermissionProvider.
  • checkPermission is pure and requires access data manually.
  • Wildcards are optional and segment-based.
  • Feature flags are exact-match only.
  • ProtectedRoute renders children, loading, or fallback UI; it does not redirect automatically.

Explainable Decisions

Accessly does not only return true or false. It returns a decision object that explains the result.

import { useAccessDecision } from "accessly";

export function ExportButton() {
  const decision = useAccessDecision("reports.export");

  if (!decision.allowed) {
    return <span>Missing: {decision.missing?.join(", ")}</span>;
  }

  return <button>Export report</button>;
}

Example decision:

{
  "allowed": true,
  "reason": "allowed",
  "requested": ["reports.export"],
  "matched": ["reports.*"],
  "checkedFrom": "wildcard"
}

Debugging Access Decisions

Use useAccessDecision when a React component needs to explain hidden UI.

import { formatDecision, useAccessDecision } from "accessly";

export function ExportDebugPanel() {
  const decision = useAccessDecision("reports.export");

  return <pre>{formatDecision(decision)}</pre>;
}

Use checkPermission outside React when you already have an access model.

import { checkPermission, formatDecision, inspectAccess } from "accessly";

const access = {
  permissions: ["reports.*"],
  flags: ["features.beta"],
};

const allowed = checkPermission(access, { permission: "reports.export" });
const denied = checkPermission(access, { permission: "billing.manage" });

console.log(formatDecision(allowed));
console.log(formatDecision(denied));
console.log(inspectAccess(access));

Allowed decisions show allowed, requested, matched, and checkedFrom. Denied decisions show reason, requested, missing, and checkedFrom. Loading decisions use reason: "not_ready".

For local development, you can copy this small debug component into your app:

import { formatDecision, useAccessDecision } from "accessly";
import type { PermissionCheckInput } from "accessly";

export function AccessDecisionDebug({
  permission,
  label = "Access decision",
}: {
  permission: string | PermissionCheckInput;
  label?: string;
}) {
  const decision = useAccessDecision(permission);

  return (
    <section aria-label={label}>
      <strong>{label}</strong>
      <pre>{formatDecision(decision)}</pre>
    </section>
  );
}

Accessly does not export this component so production UI stays yours to design.

TypeScript DX

All public APIs and types are imported from the root package.

import {
  Can,
  PermissionProvider,
  checkPermission,
  createAccessChecker,
  isAccessModel,
} from "accessly";
import type { AccessDecision, AccessModel, PermissionCheckInput } from "accessly";

Invalid permission inputs are caught by TypeScript:

const access: AccessModel = { permissions: ["users.create"] };

checkPermission(access, { permission: "users.create" });
checkPermission(access, { any: ["users.create", "users.invite"] });
checkPermission(access, { all: ["reports.view", "reports.export"] });
checkPermission(access, { flag: "features.beta" });

// TypeScript error: use `permission`, not `permissions`.
checkPermission(access, { permissions: "users.create" });

Lightweight type guards help when receiving unknown JSON. They are practical shape checks, not a full validation framework.

import { PermissionProvider, isAccessModel } from "accessly";

const data: unknown = await response.json();

if (!isAccessModel(data)) {
  throw new Error("Invalid access model");
}

<PermissionProvider access={data}>
  <App />
</PermissionProvider>;

Outside React Usage

checkPermission is pure. It requires an access model every time because it does not read React Context or any global store.

import { checkPermission, createAccessChecker } from "accessly";

const access = { permissions: ["users.create"] };

checkPermission(access, { permission: "users.create" });

const checker = createAccessChecker(access);

checker.can("users.create");
checker.decision({ flag: "features.beta" });

If your app has its own auth/session store, wrap checkPermission or createAccessChecker at the edge of that store and keep backend authorization separate.

Backend Adapter Example

Use createAdapter when your backend returns a shape that is not already an Accessly AccessModel.

import { PermissionProvider, createAdapter } from "accessly";

type BackendUser = {
  id: string;
  roles: string[];
  permissions: string[];
  featureFlags: string[];
};

const backendAdapter = createAdapter((source: BackendUser) => ({
  user: {
    id: source.id,
    roles: source.roles,
  },
  permissions: source.permissions,
  flags: source.featureFlags,
}));

export function Product({ user }: { user: BackendUser }) {
  return (
    <PermissionProvider source={user} adapter={backendAdapter}>
      <App />
    </PermissionProvider>
  );
}

Website

The public website is deployed here:

https://accessly-website.vercel.app/

Useful routes:

Monorepo Structure

This repository is a pnpm monorepo.

apps/
  website/       Website, docs, and Accessly Lab
packages/
  accessly/      Published accessly package

Development

Install dependencies:

pnpm install

Run all workspace builds:

pnpm build

Run package build:

pnpm --filter accessly build

Run website build:

pnpm --filter website build

Run tests:

pnpm test

Package Links

Security Note

Accessly controls frontend rendering. It helps React apps hide, show, explain, and organize UI based on access data.

It does not replace server-side authorization. Sensitive actions, private API routes, data fetching, mutations, billing operations, and admin actions must still be authorized on the server.

License

MIT

About

Accessly is a small React permission layer for rendering UI from a normalized access model. It provides permission components, hooks, backend adapters, navigation filtering, feature flag checks, RBAC expansion, wildcard matching, and explainable allow/deny decisions.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors