Let's upgrade our custom ABAC implementation to use CASL.
Installing CASL
First, install the CASL library:
npm install @casl/ability
We will also be using the types from the @ucast/core library:
npm install @ucast/core
Updating the Permission Module
Key Changes
Here's what changes when migrating to CASL:
// BEFORE (custom): resource first, action second
builder.allow("document", "read")
builder.allow("document", "read", { status: "published" }, ["title", "content"])
// AFTER (CASL): action first, resource second, fields before conditions
allow("read", "document")
allow("read", "document", ["title", "content"], { status: "published" })
Type Setup
CASL needs types to understand your resources which are broken down into subjects and actions:
type FullCRUD = "create" | "read" | "update" | "delete"
type ProjectSubject = "project" | Pick<Project, "department">
type DocumentSubject =
| "document"
| Pick<Document, "projectId" | "creatorId" | "status" | "isLocked">
// [Actions, Resource] tuple
type MyAbility = MongoAbility<
[FullCRUD, ProjectSubject] | [FullCRUD, DocumentSubject]
>
Using CASL in Components
The usage pattern is almost identical, with one key difference:
// Custom: pass object directly
if (permissions.can("document", "update", document)) {
// ...
}
// CASL: wrap object with subject() helper
import { subject } from "@casl/ability"
if (permissions.can("update", subject("document", document))) {
// ...
}
Note the differences:
- Resource and action order is swapped:
can("update", "document")vspermissions.can("document", "update") - Use
subject()helper to wrap data for condition checking
Why the
subject()wrapper? CASL needs to know what type of object you're checking. Our custom implementation knew because we passed the resource name first. CASL uses class names by default, but since we're using plain objects, we need to tell it explicitly.
Implementation
Let's convert our current permission checks to use CASL
CASL Benefits
CASL provides several benefits over a custom implementation:
1. Advanced Conditions
CASL supports MongoDB-style query operators:
can("read", "document", { status: { $ne: "archived" } }) // not equal
can("read", "document", { status: { $in: ["published", "draft"] } }) // in array
2. No Complicated TypeScript
CASL handles all the complex TypeScript for you, so you don't have to manually define types for each resource and action combination.
3. No Maintenance Headaches
CASL is actively maintained and widely used, so you don't have to worry about maintaining a custom permission system.
CASL Drawbacks
CASL is great at many things, but since it was created as a backend Node focused project it struggles when used with frontend frameworks like React or Next.js due to its reliance on classes.
1. Reliance on Classes
CASL uses class names to determine which permissions to check on an object. Since we're using plain objects (not class instances), permissions don't work by default which is why we need the subject() wrapper everywhere.
2. React Server Component Compatibility
The subject() function unfortunately, mutates our objects in a way that is incompatible with React Server components. There are two workarounds:
Option A: Spread into a new object
// Always create a new object before passing to subject()
ability.can("update", subject("document", { ...document }))
Option B: Use detectSubjectType
Configure CASL to detect subject types without mutation:
build({ detectSubjectType: (object) => object.__caslType })
// In your queries, add __caslType to returned objects
const document = await db.query.documents.findFirst({
where: eq(documents.id, id),
})
return { ...document, __caslType: "document" }
This approach adds a small overhead but makes consuming CASL permissions easier.
Branch Checkpoint
After completing this lesson, your code should match:
Branch: 8-casl
Run the following to sync up:
git checkout 8-casl
Summary
Migrating to CASL gives us:
- ✅ Battle-tested permission library
- ✅ Built-in field-level permissions
- ✅ Community support and documentation
The concepts are identical to what we built, but using a library like CASL can give you extra functionality and make it easier to work with on a larger team.