accesscontrol

Role- and attribute-based access control (RBAC + ABAC) for Node.js, with enforced ownership, conditions, and require() gates.

Library
npm
v3.1.0
2,328stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
80/100Excellent
Development Activity96
Maintenance72
Community52
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
85/100Excellent
Architecture88
Code Quality92
Innovation78
Learning Curve80

accesscontrol is a dependency-light Node.js library that implements both Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC) in a single, chainable API. Instead of scattering if (user.role === 'admin') checks across a codebase, applications define grants once — ac.grant('admin').createAny('video') — and query them uniformly with ac.can(role).createAny(resource), which returns a Permission object carrying a granted boolean and the resolved attribute list for filtering response data.

Version 3 turns the library into a fuller policy engine: declarative .where() conditions evaluate a readable expression syntax against per-check context ($.order.value <= 100000), own-possession checks are actually enforced against a configurable ownership resolver rather than merely selecting an attribute set, and require() gates add mandatory restrictions that can only narrow access, never grant it. Custom actions beyond CRUD, role groups and resource categories for bounded bulk grants, async condition functions, and a built-in access/change/error event emitter round out the model.

Grants can be defined programmatically, from a plain object, or restored from a flat array of database rows via getGrantsList()/AccessControl(rows), making the library a natural fit for applications that persist their permission model rather than hard-coding it. The project ships as ESM-only TypeScript with two runtime dependencies pinned to exact versions, 100% test coverage, and a mutation-testing suite (Stryker) run in CI alongside typecheck, lint, and build steps.

What You Get

  • A chainable grant/check API — ac.grant(role).createOwn(resource) and ac.can(role).createOwn(resource).granted — for defining and querying permissions without scattered conditionals.
  • ABAC conditions via .where() with a readable expression syntax (==, in, contains, matches, before/after/between/during, cidr) plus and/or/not combinators, evaluated against per-check context.
  • Enforced ownership — own-possession checks are validated against a configurable ownerField or custom resolver rather than only filtering the returned attribute set.
  • Mandatory require() gates at global, category, or resource scope that can only restrict access, layered independently on top of any grant.
  • Role groups and resource categories (admins/admin, media/photo) for bounded bulk grants — a scoped alternative to wildcard * grants.
  • Glob-notation attribute filtering (filter()) that returns a copy of a data object with only the permitted fields, including nested paths and negation.
  • A dependency-free event emitter exposing access (every resolved check, for audit logs), change, and error events.
  • Serialization helpers (getGrantsList(), getRequirements(), getVocabulary()) for persisting and restoring the full grant model from a database.

Common Use Cases

  • Enforcing per-role CRUD permissions on API resources in an Express or similar Node.js backend, replacing scattered if (user.role === ...) checks with a single grant model.
  • Enforcing record-level ownership so a user can update or delete only their own resources, with the ownership check validated by the library rather than hand-rolled per route.
  • Filtering API response payloads down to the fields a given role is permitted to see, using glob-notation attribute filtering instead of manual field stripping.
  • Applying attribute-based rules that depend on runtime context — e.g. a manager can approve orders only under a value threshold, or an editor can publish only during a scheduled window.
  • Persisting a permission model in a database as flat grant rows and rehydrating an AccessControl instance from those rows at request time.
  • Layering mandatory, non-bypassable restrictions (e.g. environment or IP-range gates) on top of an existing grant model via require().

Under The Hood

Architecture The library separates concerns into a small set of collaborating classes under src/core/: AccessControl (the public entry point and grants model owner, in src/AccessControl.ts), Access (the fluent builder returned by .grant()/.deny(), which accumulates role/resource/action/possession/condition state and commits it to the grants object on completion), Query (the builder returned by .can()), Permission (the resolved result of a query, computed eagerly in its constructor via resolveAccess() except when a custom async condition function defers resolution), and Emitter (a dependency-free pub/sub used for the access/change/error events). Grants are stored internally as a nested IGrants object and can be serialized to and from a flat row list for database persistence. Condition expressions are compiled from author-facing string sugar into a canonical JSON triple form (src/utils/condition.ts) before evaluation, keeping the stored/serialized representation stable independent of how a condition was authored. This is a layered, single-responsibility design with a clear boundary between grant construction, query resolution, and condition evaluation.

Tech Stack Written in TypeScript, published as ESM-only ("type": "module") targeting Node.js 20+. Runtime dependencies are limited to two packages by the same author, notation (glob-notation object filtering) and dtrexp (compact date-time range/recurrence expressions for .during() schedules), both pinned to exact versions rather than semver ranges. Tooling is Biome for lint/format, native tsc for typechecking and building, Vitest with the Istanbul coverage provider for tests, and Stryker for mutation testing. CI runs on GitHub Actions across Node 20/22/24, executing typecheck, lint, build, and coverage on every push, with a separate mutation-testing job.

Code Quality The test suite is extensive, spanning 20+ dedicated test files (ownership, security, async, groups, events, conditions — both unit and integration — hardening, and invariants) with the project’s own README citing 100% line coverage and an 88% mutation score, indicating the tests assert real behavior rather than just executing code paths. Error handling is explicit and typed: a custom AccessControlError carries a stable err.code and supports redacted messages, and denial reasons are surfaced structurally (e.g. reason: 'require_failed') rather than as opaque booleans. The codebase favors narrow, well-documented internal modules (utils/condition.ts, utils/validation.ts, utils/grants.ts) over a monolithic core file, and CI enforces linting and full typechecking on every change.

API Design The public API reads as a small, consistent vocabulary: CRUD sugar methods (createAny, readOwn, updateAny, deleteOwn, …) are thin wrappers over a generic .action()/.do() pair, so custom actions beyond CRUD cost nothing extra to learn. Chaining is designed to minimize repetition (.grant(role).resource(x).createAny().readAny()), and fail-closed helpers like tryCan() are provided explicitly so a failed check can never be mistaken for an allow. Documentation is comprehensive — a dedicated docs site, inline JSDoc on public classes and methods, a migration guide from v2, and runnable examples for both grant modeling and Express middleware integration — keeping the learning curve for common cases low despite the depth of the v3 policy-engine features.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search