Skip to main content

casl vs casbin vs accesscontrol: Authorization & RBAC in Node.js (2026)

·PkgPulse Team

TL;DR

CASL is the most popular isomorphic authorization library — define abilities as "can user X do Y on resource Z?", works in both Node.js and React, integrates with Prisma/MongoDB/Mongoose. Casbin is the policy engine — supports RBAC, ABAC, ACL, and custom models via a configuration DSL, used in enterprise systems across many languages. accesscontrol is the simpler RBAC library — role-based grant definitions with resource/action/possession patterns. In 2026: CASL for TypeScript-first projects, Casbin for complex multi-model authorization, accesscontrol for simple RBAC.

Key Takeaways

  • CASL: ~500K weekly downloads — TypeScript-first, isomorphic (Node + browser), Prisma/Mongoose integration
  • Casbin: ~200K weekly downloads — policy engine, PERM model, supports 15+ languages, admin UI
  • accesscontrol: ~200K weekly downloads — simple RBAC, role hierarchy, grant chains
  • CASL defines abilities: can("read", "Package"), cannot("delete", "Package", { published: true })
  • Casbin separates model (WHO can do WHAT) from policy (specific rules) — very flexible
  • accesscontrol uses grant notation: ac.grant("admin").createAny("package")

Authorization vs Authentication

Authentication: WHO are you?
  → Login, JWT, session, OAuth
  → Libraries: Passport, NextAuth, Lucia, better-auth

Authorization: WHAT can you do?
  → Permissions, roles, policies
  → Libraries: CASL, Casbin, accesscontrol (this article)

RBAC (Role-Based Access Control):
  User → has Role → Role grants Permissions
  "admin can delete any package"

ABAC (Attribute-Based Access Control):
  User attributes + Resource attributes + Context → Permission decision
  "user can edit package IF user.id === package.authorId AND package.status !== 'published'"

CASL

CASL — isomorphic authorization:

Define abilities

import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"

type Actions = "create" | "read" | "update" | "delete" | "manage"
type Subjects = "Package" | "User" | "Comment" | "all"

type AppAbility = MongoAbility<[Actions, Subjects]>

function defineAbilitiesFor(user: { id: string; role: string }) {
  const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility)

  switch (user.role) {
    case "admin":
      can("manage", "all")  // Admin can do everything
      break

    case "editor":
      can("read", "Package")
      can("create", "Package")
      can("update", "Package")  // Can update any package
      cannot("delete", "Package", { published: true })  // Can't delete published
      can("read", "Comment")
      can("delete", "Comment", { authorId: user.id })  // Own comments only
      break

    case "viewer":
      can("read", "Package")
      can("read", "Comment")
      can("create", "Comment")
      can("update", "Comment", { authorId: user.id })  // Own comments only
      can("delete", "Comment", { authorId: user.id })
      break

    default:
      can("read", "Package")  // Public: read-only
  }

  return build()
}

Check permissions

const ability = defineAbilitiesFor({ id: "user-123", role: "editor" })

// Check:
ability.can("read", "Package")                              // true
ability.can("delete", "Package")                             // true (unpublished)
ability.cannot("delete", subject("Package", { published: true }))  // true

// In Express middleware:
function authorize(action: Actions, subject: Subjects) {
  return (req, res, next) => {
    const ability = defineAbilitiesFor(req.user)
    if (ability.can(action, subject)) {
      next()
    } else {
      res.status(403).json({ error: "Forbidden" })
    }
  }
}

app.delete("/api/packages/:id", authorize("delete", "Package"), deleteHandler)

With Prisma (CASL + Prisma integration)

import { accessibleBy } from "@casl/prisma"

// Automatically filter database queries by user permissions:
app.get("/api/packages", async (req, res) => {
  const ability = defineAbilitiesFor(req.user)

  // Only returns packages the user CAN read:
  const packages = await prisma.package.findMany({
    where: accessibleBy(ability).Package,
  })

  res.json(packages)
})

// Editor sees all packages
// Viewer sees only non-draft packages
// The WHERE clause is generated automatically from CASL abilities

React integration (isomorphic)

import { Can } from "@casl/react"

function PackageCard({ pkg, ability }) {
  return (
    <div>
      <h3>{pkg.name}</h3>
      <Can I="update" a="Package" ability={ability}>
        <button onClick={() => editPackage(pkg.id)}>Edit</button>
      </Can>
      <Can I="delete" a="Package" ability={ability}>
        <button onClick={() => deletePackage(pkg.id)}>Delete</button>
      </Can>
    </div>
  )
}

Casbin

Casbin — policy engine:

Model definition (PERM)

# model.conf — RBAC model:
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

Policy rules

# policy.csv:
p, admin, package, read
p, admin, package, write
p, admin, package, delete
p, admin, user, read
p, admin, user, write

p, editor, package, read
p, editor, package, write
p, editor, comment, read
p, editor, comment, write

p, viewer, package, read
p, viewer, comment, read
p, viewer, comment, write

# Role hierarchy:
g, alice, admin
g, bob, editor
g, charlie, viewer

Node.js usage

import { newEnforcer } from "casbin"

// Load model and policy:
const enforcer = await newEnforcer("model.conf", "policy.csv")

// Check permission:
const allowed = await enforcer.enforce("alice", "package", "delete")
// true — alice is admin, admin can delete packages

const denied = await enforcer.enforce("charlie", "package", "write")
// false — charlie is viewer, viewers can't write packages

// Express middleware:
function casbinAuth(obj: string, act: string) {
  return async (req, res, next) => {
    const allowed = await enforcer.enforce(req.user.username, obj, act)
    if (allowed) {
      next()
    } else {
      res.status(403).json({ error: "Forbidden" })
    }
  }
}

app.delete("/api/packages/:id", casbinAuth("package", "delete"), deleteHandler)

ABAC model (attribute-based)

# model.conf — ABAC:
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub_rule, obj_rule, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = eval(p.sub_rule) && eval(p.obj_rule) && r.act == p.act
// Check with attributes:
const allowed = await enforcer.enforce(
  { role: "editor", department: "engineering" },
  { type: "package", status: "draft" },
  "delete"
)

Database adapter (persist policies)

import { newEnforcer } from "casbin"
import TypeORMAdapter from "typeorm-adapter"

// Store policies in PostgreSQL:
const adapter = await TypeORMAdapter.newAdapter({
  type: "postgres",
  host: "localhost",
  port: 5432,
  database: "casbin",
})

const enforcer = await newEnforcer("model.conf", adapter)

// Add policy at runtime:
await enforcer.addPolicy("dave", "package", "read")

// Remove policy:
await enforcer.removePolicy("dave", "package", "read")

accesscontrol

accesscontrol — simple RBAC:

Define grants

import { AccessControl } from "accesscontrol"

const ac = new AccessControl()

// Admin:
ac.grant("admin")
  .createAny("package")
  .readAny("package")
  .updateAny("package")
  .deleteAny("package")
  .createAny("user")
  .readAny("user")
  .updateAny("user")
  .deleteAny("user")

// Editor:
ac.grant("editor")
  .createOwn("package")
  .readAny("package")
  .updateOwn("package")         // Own packages only
  .deleteOwn("package")
  .readAny("comment")
  .createOwn("comment")
  .updateOwn("comment")
  .deleteOwn("comment")

// Viewer (extends editor's read permissions):
ac.grant("viewer")
  .readAny("package")
  .readAny("comment")
  .createOwn("comment")
  .updateOwn("comment")

// Role hierarchy:
ac.grant("superadmin").extend("admin")

Check permissions

// Check:
const permission = ac.can("editor").createOwn("package")
console.log(permission.granted)    // true
console.log(permission.attributes) // ["*"] (all attributes)

// With attribute filtering:
ac.grant("viewer").readAny("package", ["name", "version", "downloads"])
// Viewers can only see name, version, downloads — not internal fields

const perm = ac.can("viewer").readAny("package")
const filtered = perm.filter(packageData)
// Returns only { name, version, downloads } — strips other fields

Express middleware

function authorize(action: string, resource: string) {
  return (req, res, next) => {
    const permission = ac.can(req.user.role)[action](resource)

    if (!permission.granted) {
      return res.status(403).json({ error: "Forbidden" })
    }

    // Filter response attributes:
    req.permission = permission
    next()
  }
}

app.get("/api/packages", authorize("readAny", "package"), async (req, res) => {
  const packages = await PackageService.getAll()
  // Filter each package to only allowed attributes:
  const filtered = packages.map(pkg => req.permission.filter(pkg))
  res.json(filtered)
})

Feature Comparison

FeatureCASLCasbinaccesscontrol
RBAC
ABAC
Isomorphic (browser)
Prisma integration
React components✅ (<Can>)
Policy DSL
Database persistenceVia Prisma✅ (adapters)Manual
Multi-languageJS/TS only15+ languagesJS/TS only
Admin UI
TypeScript✅ First-class⚠️
Weekly downloads~500K~200K~200K

When to Use Each

Choose CASL if:

  • TypeScript-first project — best type inference for abilities
  • Need isomorphic authorization (same rules in Node.js AND React)
  • Using Prisma — accessibleBy() auto-filters database queries
  • Want ABAC with MongoDB-style conditions ({ authorId: user.id })

Choose Casbin if:

  • Complex authorization requirements (RBAC + ABAC + custom models)
  • Multi-language system — same Casbin model across Go, Java, Python, Node.js
  • Need a policy admin UI for non-developers to manage permissions
  • Want to store policies in a database with hot-reload

Choose accesscontrol if:

  • Simple RBAC is sufficient — roles → resources → actions
  • Need attribute filtering (only return certain fields based on role)
  • Own vs Any distinction is your primary access pattern
  • Straightforward, minimal API

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on @casl/ability v6.x, casbin v5.x, and accesscontrol v2.x.

Compare authorization and security packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.