Let's summarize when ABAC works well and when you should consider alternatives or simplifications.

Where ABAC Excels

ABAC is the right choice when your permissions are:

Characteristic Example
Attribute-driven "Users can edit their own unlocked draft documents"
Fine-grained Permissions depend on resource state, ownership, or context
Environment-aware Rules change based on time, location, or device
Field-level restrictions Different roles see or modify different fields on the same resource

Where ABAC Struggles

Despite its power, ABAC introduces challenges that RBAC doesn't have:

1. TypeScript Complexity

As your application grows and you add more resources/conditional types, maintaining the type system becomes increasingly difficult:

// Started manageable
type Resources = {
  project: {
    action: "create" | "read" | "update" | "delete"
    data: Project
    condition: Pick<Project, "department">
  }
  document: {
    action: "create" | "read" | "update" | "delete"
    data: Document
    condition: Pick<Document, "projectId" | "creatorId" | "status" | "isLocked">
  }
}

// Then you add complex condition operators
type Condition<T> = { $gte: T } | { $lte: T } | { $between: [T, T] } |  { $ne: T } | { $in: T[] }

// The permission builder now needs to handle all these extra condition operators
type Permission<Res extends keyof Resources> = {
  action: Resources[Res]["action"]
  condition?: // ??? This gets massive
  fields?: (keyof Resources[Res]["data"])[]
}

// The can function also needs to handle all these conditions now
can() {
  const validData = // Way more complex
}

Every new condition operator or resource type requires updating multiple type definitions, and the cognitive overhead grows with each addition.

2. Over-Engineering for Simple Use Cases

Many applications don't actually need advanced ABAC features:

  • Field-level permissions
  • Complex conditions
  • Automatic query generation

If your application only needs simple attributes checks then a basic ABAC implementation (without field filtering and complex operators) dramatically reduces complexity while still being more flexible than RBAC.

// Full ABAC with all the bells and whistles
builder.allow(
  "document",
  "update",
  { creatorId: user.id, isLocked: false, status: { $in: ["draft", "review"] } },
  ["title", "content", "status"],
)

// Basic ABAC - still powerful, much simpler
builder.allow("document", "update", { creatorId: user.id, isLocked: false })

Basic ABAC gives you:

  • Attribute-based conditions (ownership, status checks)
  • The unified can() API
  • Type safety for resources and actions

Without the overhead of:

  • Field-level permission tracking
  • Complex condition operators
  • Automatic query generation
  • Multiple layers of TypeScript generics

What's Next

The next logical step (if you need these complex ABAC features) is to reach for a library that manages all the complexities behind the scenes for you. We will be looking at converting our entire application to CASL which will reduce the amount of complex TypeScript code in our application and give us many new features we were missing before.