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.
답글 남기기