Skip to Content
AuthorizationOverview

Unreleased changes

These pages document main, which has changes 0.1.2 does not. npm install @evanion/acl gives you 0.1.2, documented here. Values on these pages are produced by running the current source on every build.

@evanion/acl

In most applications, authorization rules are written twice: once in the backend handler to guard the data, and once in the frontend to hide or show buttons. The moment a rule changes, these two copies drift, leading to a frustrating experience where a user sees a button they aren’t allowed to click, or a feature they should have access to is hidden.

@evanion/acl solves this by allowing you to write your authorization rules exactly once and share them across every process in your stack.

See the authorization tour for live demos of these concepts.

One set of rules, shared everywhere

Instead of duplicating logic, @evanion/acl keeps your rules in a single JSON document called the matrix.

Your server defines the policy and builds the matrix in memory. Any other process—such as a browser or a microservice—can then hold a copy of that matrix and answer authorization questions locally, with no network call back to the server.

Every example on these pages is set in Baize, a board game shop, where a customer who asked a question may be allowed to edit it:

import { } from '@evanion/acl'; type = { : string; : string }; const = <{ : string }, { : }>() .('question', () => .('update', .('object.askedBy', 'subject.id')), ) .(); const = .({ : 'customer-41' }, 'question', 'update', { : 'q7', : 'customer-41', }); .; // -> true

Here is how the code above works:

  • policy() initializes the rule-set.
  • .for('question', (p) => …) defines the rules for a specific object kind (in this case, question).
  • p.allow('update', …) grants permission if the provided conditions are met. For example, p.eq('object.askedBy', 'subject.id') ensures only the original author can edit the question.
  • .build() returns the evaluator (which we call access), the object used to check permissions.
  • access.can(subject, kind, action, row) is the final check: “Can this user perform this action on this specific record?”

The access.matrix property is the raw JSON representation of these rules. By passing this JSON to hydratePolicy in another process, you can recreate the exact same evaluator in the browser:

One matrix, two evaluators. Only the server's answer guards the write.

Because the answer is computed locally, the UI responds instantly. When rules change on the server, the browser simply fetches the new matrix to stay in sync.

The acl vocabulary

To help you navigate the rest of these docs, here are the core terms we use:

  • Subject: The entity acting (e.g., the logged-in user), typically resolved by your auth layer.
  • Object: The resource being acted upon. This consists of a kind (like question) and optionally a row ( a specific record). A row’s properties are its fields.
  • Permission: A pair of an object kind and an action (e.g., question.delete).
  • Condition: A logic check comparing a path in the subject or row to a value (e.g., object.askedBy must equal subject.id).
  • Rule: A set of conditions. Allow rules grant access; deny rules refuse it.
  • Default Deny: If no allow rule matches a request, the permission is refused by default.
  • Matrix: The complete set of rules serialized as a JSON object.
  • Evaluator: The engine that uses a matrix to answer can() requests.
  • Decision: The result of a can() call. It includes whether the action was allowed and the reason why (e.g., denied or no-rule-matched).

What acl can do

@evanion/acl handles a wide range of authorization patterns locally:

The questionThe answer
May this subject take this action on this row?a single decision with a specific reason
May they take it on each of these rows?one decision per row, returned in a single call
What may they do at all, with no row in hand?a full list of every permission in the matrix
Which fields of this write may they set?a decision per field, returning only the approved values
Why was this refused?the specific rule that caused the refusal
Who gained or lost access when the rules changed?a detailed report comparing the old and new matrices
May a service answer from a matrix another one built?yes, as long as it has a copy of the JSON

When acl is the wrong tool

While powerful, @evanion/acl is not designed for every security task:

  • Secret Rules: The matrix is sent to the browser, meaning all object kinds, action names, and condition values are public. Rules that must remain secret must be written in your server code.
  • Database Filtering: The library makes decisions on individual instances. Generating complex WHERE clauses for database queries is out of scope.
  • Dynamic Functions: Conditions must be declarative so they can be serialized to JSON. You cannot put a JavaScript function inside a rule.
  • Non-JS Backends: Only the JSON is portable. If your backend is in Go or .NET, you would need a compatible evaluator for those languages.

For more detailed constraints, see What @evanion/acl does not do.

A browser answer is advisory

Remember that a browser-side check is only for the UI. Because the browser environment can be manipulated, a can() call in the browser only tells you what to draw. You MUST still call can() in every server process that handles the actual request to ensure the action is truly authorized. The security contract explains this boundary in detail.

Install acl and write a policy of your own

Now that you understand the mental model, you can start implementing it. Getting started will guide you through installing the package, building your first policy, and sharing it with a browser.

Last updated on