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.
Getting started
Before you can secure your application, you need a way to define who is allowed to do what. In @evanion/acl, this is called a policy.
By the end of this page, you will have written your first policy. You’ll see how to grant the bookseller role the ability to delete questions while ensuring customers are refused, and you’ll learn how to rebuild that same logic from a JSON snapshot.
Prerequisites
To follow this guide, ensure you have the following:
- Node 20 or newer.
- An ESM project: Your
package.jsonmust include"type": "module", as@evanion/acldoes not ship a CommonJS build. - TypeScript 5.5 or newer (Optional): While the package runs in plain JavaScript, TypeScript allows the compiler to check your object kinds and actions for errors.
- A way to run TypeScript: We use
npx tsxin the examples below, which handles TypeScript execution on the fly.
Note that @evanion/acl is completely self-contained. There are no API keys to manage, no external services to run, and no environment variables to configure—the policy is built directly within your own process.
Install acl
Run the command for your preferred package manager:
npm install @evanion/aclyarn add @evanion/aclpnpm add @evanion/aclCreate your first policy
Defining an authorization policy in @evanion/acl is a straightforward three-step chain:
- Name your types: Use
policy<Subject, Objects>()to define the shape of the user (Subject) and the kinds of objects being protected (Objects). - Define the rules: Use
.for(kind, block)to write rules for a specific object kind. - Build the evaluator: Call
.build()to create theaccessobject, which you will use to check permissions.
Within the .for() block, you use a helper (conventionally named p) to grant or deny access:
p.allow(action, ...conditions): Grants access if all provided conditions are true.p.contains(path, value): A condition that checks if an array (at a given path) contains a specific value (e.g., checking ifsubject.rolescontains'bookseller').
Let’s put this into practice. In our example shop, Baize, only staff members with the bookseller role should be allowed to delete questions. Save the following as access.ts:
import { } from '@evanion/acl';
// Anyone signed in to Baize: a customer, or a bookseller on the shop's staff.
type = { : string; : string[] };
type = { : string; : string };
// 1. Define your policy once, at module scope, when the server loads this module.
const = <, { : }>()
.('question', () =>
.('delete', .('subject.roles', 'bookseller')),
)
.();
const = { : 'staff-3', : ['bookseller'] };
const = { : 'customer-41', : ['customer'] };
const = { : 'q7', : 'customer-41' };
// 2. Evaluate locally and instantly
.(, 'question', 'delete', ).; // -> true
.(, 'question', 'delete', ).; // -> false
.(, 'question', 'delete', ).; // -> 'no-rule-matched'To see it in action, wrap each line ending in // -> in a console.log and run npx tsx access.ts. You should see true, false, and no-rule-matched printed to your console.
The no-rule-matched result is the default deny behavior explained in the Overview—if no rule explicitly allows an action, it is refused.
Share your policy with the browser
One of the most powerful features of @evanion/acl is that the server and browser can share the exact same rules without duplicating code.
The access.matrix property contains the policy as a plain JSON document. You can send this JSON to the browser, where hydratePolicy rebuilds a fully functional evaluator.
Update the first line of access.ts to include hydratePolicy, and add these lines to the end of the file:
// The server sends the matrix as JSON text, and the browser rebuilds from it.
const = .(.);
const = (.());
.(, 'question', 'delete', ).; // -> true
.(, 'question', 'delete', ).; // -> falseRun the file again. The results will match the server’s answers exactly. In a real application, your server would send this JSON string in a page or API response, allowing the browser to make instant, local authorization decisions without calling the server.
Note: When using hydratePolicy without type arguments, the evaluator accepts any object kind or action string, meaning the TypeScript compiler can no longer verify them.
Next steps: Refining your rules
You now have a working policy that grants access by role and can be shared across processes.
In the next section, Rules about who the user is, we’ll explore how to handle more complex scenarios—such as preventing a suspended bookseller from reading stock reports—and how to name your rules so that your application can explain exactly why a request was refused.