DEV Community

Alaa Samy
Alaa Samy

Posted on

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");
Enter fullscreen mode Exit fullscreen mode

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 looked around for something to fix this and everything I found was either React/Next.js-only or big enough that I'd have spent longer reading the docs than fixing the actual problem. I wanted something small enough to 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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. No wildcard permissions, no feature flags, no explainable "why was this denied" output — just boolean checks. 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 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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (5)

Collapse
 
topstar_ai profile image
Luis Cruz

Great example of solving a small but painful engineering problem. Permission systems often become messy over time because strings spread across services, teams, and codebases without a single source of truth.

A lightweight permission library can create a consistent contract between the backend and application logic, reducing bugs and making authorization rules easier to understand and maintain.

The interesting challenge as it grows will be keeping it flexible without turning it into an overly complex framework. Good abstractions should remove friction while staying close to the real domain needs. Nice work turning an internal pain point into a reusable tool!

Collapse
 
alaa-samy profile image
Alaa Samy

Thanks, really appreciate that!
And totally agree, keeping it flexible without letting it turn into a framework is exactly the balance I'm trying to hold onto as it grows.

Collapse
 
frank_signorini profile image
Frank

How did you handle nested roles with permission-access, and are there any plans to add support for attribute-based access control? I'd love to swap ideas on this.

Collapse
 
alaa-samy profile image
Alaa Samy

Good question! Right now permission-access doesn't resolve roles itself, it just expects a flat, already-resolved permission list.
No ABAC support yet either, but I'd love to consider both for the next version. Would be great to swap ideas if you've got specific use cases in mind.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.