CASL is an isomorphic authorization library — the same permission rules run on both server and browser. AbilityBuilder with can("read", "Article") and cannot("delete", "Article", { published: true }) defines rules declaratively. ability.can("update", article) checks if a user can perform an action on a subject. subject("Article", articleObject) tells CASL which subject type a plain object belongs to for attribute-level checks. createMongoAbility supports MongoDB-style conditions for field-level filtering. packRules/unpackRules serializes abilities for JWT storage. useAbility from @casl/react provides a React hook for permission checks in components. accessibleBy(ability, "read") returns a Prisma query filter for only accessible records. Claude Code generates CASL ability definitions, role-to-permission mappings, React ability providers, Prisma accessibleBy integration, and Next.js middleware permission guards.
CLAUDE.md for CASL
## CASL Stack
- Version: @casl/ability >= 6.7, @casl/react >= 3.1, @casl/prisma >= 1.5
- Define: const { can, cannot, build } = new AbilityBuilder(createMongoAbility)
- Check: ability.can("update", article) — or ability.can("update", subject("Article", articleData))
- React: const ability = useAbility(AbilityContext) — wrap app in <AbilityContext.Provider value={ability}>
- Conditions: can("read", "Article", { published: true }) — MongoDB $gt/$in/$regex operators
- Prisma: accessibleBy(ability, "read").Article — returns Prisma where clause
- Pack: packRules(ability.rules) → store in JWT → unpackRules(rules) for client
- Update: ability.update(newRules) — refresh after role change
Ability Definition
// lib/casl/ability.ts — define permissions per role
import { AbilityBuilder, createMongoAbility, MongoAbility, ForcedSubject } from "@casl/ability"
// Subject types must match your domain models
type SubjectTypes =
| "Article" | { kind: "Article"; authorId: string; published: boolean }
| "Comment" | { kind: "Comment"; authorId: string }
| "User" | { kind: "User"; id: string }
| "Order" | { kind: "Order"; customerId: string; status: string }
| "all"
type Actions = "create" | "read" | "update" | "delete" | "manage" | "publish" | "approve"
export type AppAbility = MongoAbility<[Actions, SubjectTypes]>
type User = {
id: string
role: "guest" | "user" | "editor" | "admin"
organizationId?: string
}
// Build abilities based on user role
export function defineAbilityFor(user: User | null): AppAbility {
const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility)
if (!user) {
// Unauthenticated — read-only on published content
can("read", "Article", { published: true })
can("read", "Comment")
return build()
}
switch (user.role) {
case "admin":
// Admins can manage everything
can("manage", "all")
break
case "editor":
// Editors can manage articles and comments
can("manage", "Article")
can("manage", "Comment")
can("read", "User")
// But can't delete other editors/admins
cannot("delete", "User", { role: { $in: ["editor", "admin"] } })
break
case "user":
// Regular users — read public content
can("read", "Article", { published: true })
can("read", "Comment")
// Own articles — can update/delete only their own
can(["create", "read", "update", "delete"], "Article", { authorId: user.id })
can(["create", "read", "update", "delete"], "Comment", { authorId: user.id })
// Own orders
can("read", "Order", { customerId: user.id })
// Own profile
can(["read", "update"], "User", { id: user.id })
break
case "guest":
can("read", "Article", { published: true })
can("read", "Comment")
break
}
return build()
}
// For permission checks with typed subjects
export function subjectOf<T extends object>(type: string, subject: T) {
return Object.assign(subject, { kind: type } as { kind: string }) as T & ForcedSubject<string>
}
React Integration
// lib/casl/context.tsx — React ability context
"use client"
import { createContext, useContext, useState, useCallback } from "react"
import { createMongoAbility, type MongoAbility } from "@casl/ability"
import { AblityContext } from "@casl/react"
import type { AppAbility } from "./ability"
// Create a null ability for initial state
export const createNullAbility = (): AppAbility =>
createMongoAbility([]) as AppAbility
export const AbilityContext = createContext<AppAbility>(createNullAbility())
interface AbilityProviderProps {
ability: AppAbility
children: React.ReactNode
}
export function AbilityProvider({ ability, children }: AbilityProviderProps) {
return (
<AbilityContext.Provider value={ability}>
{children}
</AbilityContext.Provider>
)
}
export function useAbility() {
return useContext(AbilityContext)
}
// components/Can.tsx — declarative permission check component
import { createContextualCan } from "@casl/react"
export const Can = createContextualCan(AbilityContext.Consumer)
// Usage: <Can I="delete" a="Article" this={article}>
// <DeleteButton />
// </Can>
// app/layout.tsx — provide ability from session
import { currentUser } from "@clerk/nextjs/server" // or your auth
import { defineAbilityFor } from "@/lib/casl/ability"
import { AbilityProvider } from "@/lib/casl/context"
import { packRules } from "@casl/ability/extra"
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const clerkUser = await currentUser()
const user = clerkUser ? {
id: clerkUser.id,
role: (clerkUser.publicMetadata.role as string ?? "user") as "user" | "admin" | "editor" | "guest",
} : null
const ability = defineAbilityFor(user)
const packedRules = packRules(ability.rules)
return (
<html>
<body>
<AbilityProvider ability={ability}>
{/* Pass packed rules to client for hydration */}
<script
id="casl-ability"
type="application/json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(packedRules) }}
/>
{children}
</AbilityProvider>
</body>
</html>
)
}
Permission Guards in Components
// components/ArticleActions.tsx — conditional UI based on permissions
"use client"
import { useAbility } from "@/lib/casl/context"
import { Can } from "@/lib/casl/context"
import { subject } from "@casl/ability"
type Article = { id: string; title: string; authorId: string; published: boolean }
export function ArticleActions({ article }: { article: Article }) {
const ability = useAbility()
const articleSubject = subject("Article", article)
return (
<div className="flex gap-2">
{/* Declarative with Can component */}
<Can I="update" a="Article" this={articleSubject}>
<a href={`/articles/${article.id}/edit`} className="btn-outline text-sm">
Edit
</a>
</Can>
{/* Imperative with useAbility hook */}
{ability.can("publish", articleSubject) && (
<button className="btn-secondary text-sm">
{article.published ? "Unpublish" : "Publish"}
</button>
)}
<Can I="delete" a="Article" this={articleSubject}>
<button className="btn-danger text-sm">Delete</button>
</Can>
</div>
)
}
// Higher-order guard component
export function RequirePermission({
action,
subject: subjectType,
subjectData,
children,
fallback = null,
}: {
action: string
subject: string
subjectData?: object
children: React.ReactNode
fallback?: React.ReactNode
}) {
const ability = useAbility()
const target = subjectData ? subject(subjectType, subjectData) : subjectType
if (!ability.can(action as any, target as any)) {
return <>{fallback}</>
}
return <>{children}</>
}
Prisma Integration
// lib/db/accessible-queries.ts — filter queries by ability
import { accessibleBy } from "@casl/prisma"
import { db } from "./client"
import type { AppAbility } from "@/lib/casl/ability"
// Only return articles the user can read
export async function getAccessibleArticles(ability: AppAbility, options?: {
limit?: number
cursor?: string
}) {
return db.article.findMany({
where: accessibleBy(ability, "read").Article, // Returns Prisma where clause
take: options?.limit ?? 20,
cursor: options?.cursor ? { id: options.cursor } : undefined,
orderBy: { createdAt: "desc" },
include: { author: { select: { id: true, name: true, avatarUrl: true } } },
})
}
// Combine ability filter with other conditions
export async function searchAccessibleArticles(
ability: AppAbility,
query: string,
) {
return db.article.findMany({
where: {
AND: [
accessibleBy(ability, "read").Article, // Permission filter
{
OR: [
{ title: { contains: query, mode: "insensitive" } },
{ content: { contains: query, mode: "insensitive" } },
],
},
],
},
orderBy: { createdAt: "desc" },
})
}
// Server Action with ability check
export async function deleteArticle(ability: AppAbility, articleId: string) {
const article = await db.article.findUniqueOrThrow({ where: { id: articleId } })
if (!ability.can("delete", { kind: "Article", ...article })) {
throw new Error("Forbidden: cannot delete this article")
}
return db.article.delete({ where: { id: articleId } })
}
For the next-safe-action alternative when Server Action type safety with zod schema validation, permission checks via middleware, and action metadata for role guards are needed without a full CASL setup — next-safe-action provides a lighter abstraction over Server Actions with built-in authorization middleware, see the Server Actions guide. For the role-based middleware alternative when simple role checks on routes — if (user.role !== "admin") redirect("/403") — without attribute-level permissions or subject types are sufficient, CASL is overkill and session-based role guards in middleware are simpler to reason about. The Claude Skills 360 bundle includes CASL skill sets covering role definitions, React integration, and Prisma query filtering. Start with the free tier to try permission system generation.