DEV Community

I built a tiny permission-checking library because our backend's permission strings never matched what the UI expected

A few weeks ago I was working on an internal admin dashboard (Next.js, Redux, the usual stack) and kept running into the same annoying pattern: the backend didn't hand out permission strings like orders.read. It handed out stuff like Orders.GetAll_GET and Orders.Create_POST - literally named after the endpoint.

Every module had its own slightly different set of these too. So every "can the user do X" check in the UI ended up looking like this:

const canRead = userPermissions.includes("Orders.GetAll_GET");

Copy that forty times across a dashboard with modules for orders, deals, users, roles, promo codes... and you've got forty places where a backend rename silently breaks a permission check, and nobody notices until QA does.

I went looking for a library to fix this and found accessly, which is genuinely solid - wildcard matching, decision objects that explain why something was denied, adapters for different backend shapes. But it's React/Next.js only, and honestly more than I needed for a side project. I wanted something small enough to actually read in one sitting.

So I wrote permission-access. It's tiny, has zero runtime dependencies, and the core doesn't know or care that React exists.

The actual idea

The one thing I wanted to solve for real: let the UI ask a semantic question ("can they read Orders?") without hardcoding whatever ugly endpoint name the backend picked for that module.

import { createAccessChecker, createActionMap } from "permission-access";

const actionMap = createActionMap({
  read: ["GetAll_GET", "GetList_GET"],
  create: ["Create_POST"],
});

const checker = createAccessChecker(
  { permissions: ["Orders.GetAll_GET"] },
  { actionMap },
);

checker.can("Orders", "read"); // true

The actionMap is completely optional though. If you don't pass one, canRead("Orders") just checks the literal string "Orders.read" - no assumptions baked in about your backend's naming.

I went back and forth on this because my first draft did ship a default REST-shaped map, and someone pointed out (rightly) that baking in an opinionated default is exactly the kind of thing that silently breaks for anyone whose backend isn't shaped like mine. So now it's opt-in, via a preset called REST_ACTION_MAP if you want it.

Using it without React

const checker = createAccessChecker({
  permissions: ["Orders.read", "Orders.create"],
  isAdmin: false,
});

checker.canRead("Orders");   // true
checker.canCreate("Orders"); // true
checker.canUpdate("Orders"); // false

Using it with React

import { PermissionsProvider, usePermissions, Can } from "permission-access/react";

function App({ user }) {
  return (
    <PermissionsProvider permissions={user.permissions} isAdmin={user.isAdmin}>
      <OrdersPage />
    </PermissionsProvider>
  );
}

function OrdersPage() {
  const { canCreate } = usePermissions();

  return (
    <div>
      <Can module="Orders" action="read" fallback={<p>No access</p>}>
        <OrdersTable />
      </Can>
      {canCreate("Orders") && <CreateOrderButton />}
    </div>
  );
}

That's basically the whole API surface: createAccessChecker, has, can, a handful of canRead/canCreate/canUpdate convenience wrappers, and <PermissionsProvider> / <Can> on the React side.

What it isn't

I want to be upfront that this is a small library, not a replacement for something like accessly if you need wildcard permissions, feature flags, or explainable "why was this denied" output. It's v0.1.x, I wrote the test suite myself, and it hasn't been battle-tested outside my own project yet. If you need something more complete today, accessly is worth a look.

If you just want a small, framework-agnostic permission checker that doesn't assume anything about your backend's naming, it's on npm:

npm install permission-access

Repo's here if you want to poke at the source or file an issue:
https://github.com/alaasamy347/permission-access

Happy to hear what breaks for you if you try it.

Comments

No comments yet. Start the discussion.