casl vs casbin vs accesscontrol: Authorization & RBAC in Node.js (2026)
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
| Feature | CASL | Casbin | accesscontrol |
|---|---|---|---|
| RBAC | ✅ | ✅ | ✅ |
| ABAC | ✅ | ✅ | ❌ |
| Isomorphic (browser) | ✅ | ❌ | ❌ |
| Prisma integration | ✅ | ❌ | ❌ |
| React components | ✅ (<Can>) | ❌ | ❌ |
| Policy DSL | ❌ | ✅ | ❌ |
| Database persistence | Via Prisma | ✅ (adapters) | Manual |
| Multi-language | JS/TS only | 15+ languages | JS/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.