@evanion/react-acl
Not on npm yet
npm install @evanion/react-acl does not resolve. The package is private: true, so a release run versions and tags it without publishing: npm cannot configure a trusted publisher for a package that does not exist on the registry, and the first version has to go up by hand.
The provider, the four hooks and the typed-policy factory are complete and
covered by libs/react-acl/src. Every example in this section is a region
of libs/react-acl/examples/ and runs under nx test @evanion/react-acl.
Inside this repository the package resolves from source like any other
workspace package.
The concept. @evanion/react-acl holds one evaluator, one subject and one
clock instant in a React context.
What you get. A button four levels down that calls useCan and names none
of the three.
Why you want it. A component passed now by hand reads a different instant
from its sibling, and a time-windowed rule then answers two ways on one screen.
How the library gets you there. PolicyProvider takes access, subject
and context once, and four hooks read all three.
One provider holds what every hook reads
@evanion/react-acl holds one evaluator, one subject and one instant in a React
context. PolicyProvider takes access, subject and context once. useCan,
useCanMany, useCanFields and useCapabilities read all three out of that
context and call the core’s method of the same name.
Baize, the board game shop, draws an Edit button four components below its listing route, and that button asks with three strings and the row it is drawing.
Without the provider, access, subject and now arrive as props on
ShopPage, ListingView and ActionBar, none of which read them. A component
that threads access and forgets now reads every time window in its branch
against a different instant.
'use client';
import { } from '@evanion/react-acl';
type = { : string; : string; : 'draft' | 'published' };
export function ({ }: { : }) {
const = ('listing', 'edit', );
if (!.) return null;
return < ="button">Edit listing</>;
}EditControl names the object kind, the action and the listing. The shopper and
the instant are in the provider above it, and decision is the same decision
can returns on a server, with the same allowed and reason
on it.
A decision in a browser enforces nothing
Every hook here toggles what a reader sees. A hidden Edit button is still a URL, and the shopper can post to it. Enforcement happens on the trusted side of a boundary, in every layer, each one deciding for itself. Which side decides is the short version and the security contract is the whole of it.
When to reach for react-acl
@evanion/react-acl suits three shapes of app:
- A client tree that asks about permissions at more than one depth, where
accessandsubjecthave started arriving as props in components that do not read them. - A React Router 8 or Next.js app that already sends the matrix to the browser and wants the client half to read it without rebuilding an evaluator per component.
- A screen whose shape is the answer: a form whose inputs are separately writable, a menu whose entries are whatever the shopper may reach, a table of rows each with its own answer, an action bar that changes when the row changes.
When react-acl is the wrong tool
@evanion/react-acl answers nothing useful in three places:
- A server component, a loader or a Server Action. Those call
access.candirectly. The package carries'use client'at the top of its entry point and callscreateContext, so it cannot run in a React Server Component at all. - One component asking one question. A provider for a single call site is a
context you have to keep mounted, and
access.can(subject, key, action, row)is the same answer in one line. - Anywhere the answer has to hold. The hooks read a copy of the matrix that is as fresh as the render that delivered it, and a browser evaluating a stale one keeps granting what the server has revoked.
What the react-acl package exports
@evanion/react-acl exports six values and three types:
| Export | What it is |
|---|---|
PolicyProvider | holds access, subject and context for the tree under it |
useCan(key, action, object?) | one decision |
useCanMany(key, action, objects) | a decision per row, parallel to the array |
useCanFields(key, action, object, axis, proposed?) | the field-level decision on one axis |
useCapabilities() | every action-level decision for the current subject |
createPolicyContext(access) | a provider and the same four hooks, at a typed policy’s types |
PolicyProviderProps | the provider’s props, for a wrapper that names them |
PolicyContext | what createPolicyContext returns |
BoundPolicyProviderProps | the typed provider’s props, where access is optional |
@evanion/react-acl re-exports every core type those signatures name under the
same name: Decision, FieldDecision, Access, Subject, Matrix and the
rest. The core’s values stay in @evanion/acl, so import hydratePolicy and
policy from there.
React 18 or 19 is the only peer dependency and @evanion/acl is a dependency.
The package is ESM only.
Where to go next
- Getting started — mount the provider, read a decision, and drive a tree from the role switch
- Which side decides — what crosses a server render, and what a hook is allowed to be trusted for
- API reference — the provider, the four hooks, the factory that keeps a typed policy’s keys, and what each one memoises on
The rules themselves are the core’s:
- Rules that read the subject — writing a policy, and what a decision is
- Can this user do this? — the decision shape
useCanhands back - Security contract — the trust boundary this package sits on the wrong side of