Claude Code for CASL: Isomorphic Authorization for JavaScript — Claude Skills 360 Blog
Blog / Backend / Claude Code for CASL: Isomorphic Authorization for JavaScript
Backend

Claude Code for CASL: Isomorphic Authorization for JavaScript

Published: April 14, 2027
Read time: 7 min read
By: Claude Skills 360

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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free