@casl/ability, @casl/react, accesscontrol, and casbin are JavaScript libraries designed to manage user permissions and enforce access control in web applications. They help developers define what actions users can perform on specific resources, and integrate those rules into UI rendering or API logic. While all aim to solve authorization, they differ significantly in architecture, syntax, ecosystem integration, and frontend suitability.
Managing what users can see or do in your app is critical — but doing it cleanly without cluttering your components is hard. The libraries @casl/ability, @casl/react, accesscontrol, and casbin each offer different approaches to this problem. Let’s compare how they define rules, check permissions, and integrate into real frontend code.
@casl/ability uses a fluent, human-readable API to define what a user can do.
// Define abilities using AbilityBuilder
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
can('read', 'Post');
can('update', 'Post', { authorId: userId });
cannot('delete', 'Post');
const ability = build();
This style feels intuitive and maps closely to business requirements like “editors can update their own posts.”
accesscontrol defines permissions as static JSON-like objects grouped by roles.
// Define grants as an object
import AccessControl from 'accesscontrol';
const ac = new AccessControl({
admin: {
post: ['create', 'read', 'update', 'delete']
},
editor: {
post: ['read', 'update']
}
});
// Or add dynamically
ac.grant('editor').execute('update').on('post');
It’s great if your roles and permissions are fixed and you want to load them from config.
casbin uses external policy files (or arrays) with a generic format like [sub, obj, act].
// Define policy in code (usually loaded from file/db)
import { newEnforcer } from 'casbin';
import { MemoryAdapter } from 'casbin';
const adapter = new MemoryAdapter([
['admin', 'post', 'read'],
['admin', 'post', 'write'],
['editor', 'post', 'read']
]);
const enforcer = await newEnforcer('model.conf', adapter);
You also need a model file (model.conf) that defines the matching logic. This is powerful for complex systems but overkill for most frontend apps.
⚠️ Note:
casbin’s JavaScript version is not optimized for frontend use. It includes large dependencies and assumes server-side execution. Avoid it in browser bundles unless absolutely necessary.
How do you actually test if a user can perform an action?
@casl/ability offers a simple method:
// Check anywhere
if (ability.can('update', post)) {
// Show edit button
}
The second argument can be a class, string, or object — and it supports field-level and conditional checks.
accesscontrol uses role + resource + action triples:
// Check permission for a role
const permission = ac.can('editor').execute('update').on('post');
if (permission.granted) {
// Allow action
}
Note: You must know the user’s role ahead of time. There’s no built-in concept of a “current user” — you manage that yourself.
casbin checks using subject, object, and action:
// Enforce policy
const allowed = await enforcer.enforce('editor', 'post', 'update');
if (allowed) {
// Proceed
}
Again, this works but brings unnecessary overhead to the frontend.
Only @casl/react provides first-class React support.
// Use the <Can> component
import { Can } from '@casl/react';
import { useAbility } from '@casl/react';
function PostActions({ post }) {
return (
<>
<Can I="update" a="Post" of={post}>
<button>Edit</button>
</Can>
<Can I="delete" a="Post">
<button>Delete</button>
</Can>
</>
);
}
// Or use the hook
function useCanEdit(post) {
const ability = useAbility();
return ability.can('update', post);
}
When the underlying ability instance changes (e.g., after login), components using <Can> or useAbility() automatically re-render.
accesscontrol and casbin have no official React bindings. You’d need to wrap their checks in useState/useEffect manually:
// Manual integration with accesscontrol (not ideal)
function PostActions({ post, userRole }) {
const [canUpdate, setCanUpdate] = useState(false);
useEffect(() => {
const permission = ac.can(userRole).execute('update').on('post');
setCanUpdate(permission.granted);
}, [userRole]);
return canUpdate ? <button>Edit</button> : null;
}
This adds boilerplate and doesn’t react to granular changes (like post ownership).
User permissions often change during a session (e.g., after role upgrade). How do these libraries handle that?
@casl/ability lets you update rules dynamically:
// Update ability instance
ability.update((can, cannot) => {
can('publish', 'Post');
});
// All <Can> components re-render automatically
accesscontrol allows adding/removing grants, but won’t notify your UI:
ac.grant('editor').execute('publish').on('post');
// Your React components won’t know unless you trigger a state update manually
casbin supports reloading policies, but again — no frontend reactivity.
@casl/ability: Tiny core (~5KB min+gzip), modular design. Only import what you need.@casl/react: Adds ~1KB on top of @casl/ability for React-specific logic.accesscontrol: ~8KB, includes full RBAC engine but no framework glue.casbin: ~30KB+, includes parser, model evaluator, and adapters — too heavy for most frontend use cases.As of 2024:
@casl/ability and @casl/react are actively maintained, with regular updates and strong TypeScript support.accesscontrol hasn’t had a major release since 2020 but remains stable and functional for basic RBAC. No deprecation notice, but development is slow.casbin’s JavaScript port is community-maintained and lags behind the Go core. The team recommends using it only on the backend.You have roles like “admin”, “editor”, and “viewer”, and want buttons to appear/disappear based on permissions.
@casl/ability + @casl/reactYour app loads a JSON permission matrix at startup and never changes it during the session.
accesscontrolYou need attribute-based rules like “managers can approve expenses under $10k”.
You just need to hide a “Delete” button for non-owners.
@casl/ability alone (no React package needed if you manage state yourself)| Feature | @casl/ability | @casl/react | accesscontrol | casbin |
|---|---|---|---|---|
| Frontend-optimized | ✅ Yes | ✅ Yes (React-specific) | ⚠️ Partially | ❌ No (backend-focused) |
| Reactive UI updates | ✅ With event emitter | ✅ Built-in | ❌ Manual only | ❌ Manual only |
| Permission syntax | Fluent DSL | Same as @casl/ability | JSON/object grants | Policy files (INI/CSV) |
| Conditional rules | ✅ Yes (e.g., { authorId }) | ✅ Yes | ❌ Role-based only | ✅ Yes (with model) |
| Bundle size | ~5KB | ~6KB | ~8KB | ~30KB+ |
| Maintenance status | ✅ Active | ✅ Active | ⚠️ Stable but slow | ⚠️ Backend-focused |
For frontend authorization, CASL is the clear winner. Start with @casl/ability for the core logic, and add @casl/react if you’re in a React app. It gives you expressive rules, tiny footprint, and automatic UI updates.
Avoid casbin in the browser — push complex policies to your backend. Use accesscontrol only if you have a simple, static role model and don’t mind manual UI updates.
Remember: authorization logic should live as close to the data as possible. Even with CASL, always validate permissions on the server — frontend checks are just for UX, not security.
Choose @casl/ability if you need a lightweight, flexible, and framework-agnostic core library for defining and checking permissions using a natural, readable DSL (e.g., 'can read Post'). It’s ideal for projects where you want fine-grained control over ability definitions and plan to integrate with React, Vue, Angular, or vanilla JS without heavy dependencies.
Choose @casl/react if you’re already using @casl/ability and building a React application that requires reactive, component-level permission checks. It provides hooks like useAbility and components like Can that automatically re-render when permissions change, making it seamless to hide or disable UI elements based on user roles.
Choose accesscontrol if you prefer a declarative, role-based access control (RBAC) model defined via JSON or object literals, and don’t need real-time reactivity in the UI. It’s well-suited for server-side or static permission setups where roles and grants are known at startup and rarely change during runtime.
Choose casbin if your project requires advanced authorization models like ABAC, RBAC, or ACL with support for complex policies stored externally (e.g., in files or databases). However, note that Casbin’s JavaScript port is primarily designed for backend use; using it in frontend apps adds significant bundle size and lacks native React integration, so it’s generally not recommended for client-side-only authorization.
This package is the core of CASL. It includes logic responsible for checking and defining permissions.
npm install @casl/ability
# or
pnpm install @casl/ability
# or
yarn add @casl/ability
This README file contains only basic information about the package. If you need an in depth introduction, please visit CASL's documentation.
Note: the best way to get started is to read the Guide in the official documentation. In this README file, you will find just basic information.
Note: all the examples below are written in ES6 using ES modules but CASL also has a sophisticated support for TypeScript, read CASL TypeScript support for details.
Lets define Ability for a blog website where visitors:
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
import { User } from '../models'; // application specific interfaces
/**
* @param user contains details about logged in user: its id, name, email, etc
*/
function defineAbilitiesFor(user) {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
// can read blog posts
can('read', 'BlogPost');
// can manage (i.e., do anything) own posts
can('manage', 'BlogPost', { author: user.id });
// cannot delete a post if it was created more than a day ago
cannot('delete', 'BlogPost', {
createdAt: { $lt: Date.now() - 24 * 60 * 60 * 1000 }
});
return build();
});
Do you see how easily business requirements were translated into CASL's rules?
And yes, Ability class allow you to use some MongoDB operators to define conditions. Don't worry if you don't know MongoDB, it's not required and explained in details in Defining Abilities
Later on you can check abilities by using can and cannot methods of Ability instance.
import { BlogPost, ForbiddenError } from '../models';
const user = getLoggedInUser(); // app specific function
const ability = defineAbilitiesFor(user)
// true if ability allows to read at least one Post
ability.can('read', 'BlogPost');
// true if there is no ability to read this particular blog post
const post = new BlogPost({ title: 'What is CASL?' });
ability.cannot('read', post);
// you can even throw an error if there is a missed ability
ForbiddenError.from(ability).throwUnlessCan('read', post);
Of course, you are not restricted to use only class instances in order to check permissions on objects. See Introduction for the detailed explanation.
CASL has a complementary package [@casl/mongoose] which provides easy integration with MongoDB and [mongoose].
import { accessibleRecordsPlugin } from '@casl/mongoose';
import mongoose from 'mongoose';
mongoose.plugin(accessibleRecordsPlugin);
const user = getUserLoggedInUser(); // app specific function
const ability = defineAbilitiesFor(user);
const BlogPost = mongoose.model('BlogPost', mongoose.Schema({
title: String,
author: mongoose.Types.ObjectId,
content: String,
createdAt: Date,
hidden: { type: Boolean, default: false }
}))
// returns mongoose Query, so you can chain it with other conditions
const posts = await Post.accessibleBy(ability).where({ hidden: false });
// you can also call it on existing query to enforce permissions
const hiddenPosts = await Post.find({ hidden: true }).accessibleBy(ability);
// you can even pass the action as a 2nd parameter. By default action is "read"
const updatablePosts = await Post.accessibleBy(ability, 'update');
See Database integration for details.
CASL is incrementally adoptable, that means you can start your project with simple claim (or action) based authorization and evolve it later, when your app functionality evolves.
CASL is composable, that means you can implement alternative conditions matching (e.g., based on joi, ajv or pure functions) and field matching (e.g., to support alternative syntax in fields like addresses.*.street or addresses[0].street) logic.
See Advanced usage for details.
CASL checks are quite fast, thanks to underlying rule index structure. The estimated complexity of different operations can be found below:
| Operation | Complexity | Notes |
|---|---|---|
Ability creation time | O(n) | n - amount of rules |
Check by action and subject type (e.g., ability.can('read', 'Todo')) | O(1) | |
Check by action and subject object (e.g., ability.can('read', todo)) | O(m + k) + O(p) | m - amount of rules for the same pair of action and subject; k - amount of operators in conditions; O(p) - complexity of used operators (e.g., $in implementation is more complex than $lt) |
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on guidelines for contributing