Skip to main content

How duck-iam compares

WIP

Where duck-iam sits against Casbin, CASL, Oso, OPA, Cedar and AccessControl.js, which claims are measured, and when to pick something else

Every comparison table lies a little, usually by omitting what it did not test. This page separates two kinds of claim: things measured in-tree against a library that is actually installed, and things read from another project's documentation. No number here is a benchmark result; those live on the benchmarks page.

What this page compares against

Measured. Five libraries are installed as devDependencies of @gentleduck/iam and benchmarked head to head in test/benchmark.bench.ts. Every performance claim about them comes from running that file:

LibraryVersion benchmarked
@casl/ability^7.0.0
casbin^5.50.0
accesscontrol^3.1.0
@rbac/rbac^2.1.3
easy-rbac^4.0.0

Not measured. Oso, OPA, Cedar, Permit.io, and Cerbos are not installed and not benchmarked. They appear below only on feature axes taken from their own documentation, and no ops-per-second figure is claimed for any of them. Where a comparison is documentation-derived, the table says so.

The space

TypeScript and Node authorization tools cluster into four archetypes, and the archetype matters more than any individual feature.

Loading diagram...

duck-iam is archetype 3, with one twist: RBAC and ABAC are not separate subsystems. Roles compile into a synthetic policy and meet your hand-written policies inside the same evaluator, under the same combining algorithms. You do not choose one model; you write both side by side.

Where the decision happens

The archetypes differ most in where the check runs and what a policy change costs.

Loading diagram...

CASL is the fastest of these because it does the least at request time: rules are frozen at build() and every check is one hash lookup. DUCK and CASBIN accept a runtime cost in exchange for policies that change without a redeploy. OPA and HOST trade microseconds for a network round trip, and get language independence in return.

That framing explains most of the benchmark gap: duck-iam is measurably slower than CASL per check and measurably faster than Casbin, and both results follow from where each one does its work.

Feature comparison

Rows marked "docs" are read from the other project's documentation rather than tested here.

duck-iamCasbinCASLOso (docs)OPA / Cedar (docs)AccessControl.js
LanguageTypeScriptTS, Go, Python, and moreTypeScriptTS with a Rust coreRego or Cedar DSLTypeScript
RBACFirst-classYes, RBAC modelYesYesYesYes
ABAC conditions19 operatorsYes, via matchersYes, MongoDB-styleYesYesNo
One pipeline for bothYes, roles compile to a policyTwo model filesOne ability setOne pipelineOne DSLRBAC only
Combining algorithms4 in-policy, 3 cross-policyCustom matcherSubject-side evaluationSingle policyWritten in the DSLNot applicable
Multi-tenant scopingFirst-class scoped rolesManual, via subjectsManualManualManualNo
Decision tracesengine.explain(), development modeLimitedLimitedinspect()OPA explainNo
Runtime policy reloadYes, adapter-driven with an invalidatorYes, watcherRebuild the abilityYesYesRebuild the grants
Type inferenceConst-typed actions, resources, roles, scopesStringly typedStrongStrongNone, it is a DSLStringly typed
Development and production modesYesNoNoNoNoNo
Server middleware shippedExpress, Hono, NestJS, Next.js, genericBring your ownBring your ownBring your ownBring your ownBring your own
Client packages shippedReact, Vue, vanillaNoReact Can componentNoNoNo
Storage adapters shippedMemory, file, Prisma, Drizzle, Redis, HTTP20+ across languagesNoNoNoNo
Built-in cachingLRU with TTL, single-flighted, cross-instance invalidatorNoNot needed, rules are in memoryNoBundle cacheNo
Batch permission checksengine.permissions()NoNoNoPartial evaluationNo
Rule indexingPer-policy Map, held in a WeakMapNoYesYesYesNo
Runtime dependencies1, uuid, only for the Drizzle schemas50native bindingseparate binary1
Measured hereYesYesYesNoNoYes
LicenseMITApache 2.0MITApache 2.0Apache 2.0MIT

The duck-iam column is read from the 5.10.0 source and package.json. Competitor dependency counts and licenses come from each project's own published metadata, checked 2026-03-30, and go stale when they release. For the numbers behind the "measured here" row, see benchmarks.

Two smaller libraries appear in the benchmark suite without a column here, because their surface is too narrow for the axes above. easy-rbac is roughly 2 KB and needs no configuration beyond a hierarchical role object: a good fit when hierarchical RBAC is the entire requirement, and it benchmarks faster than duck-iam's own iamEvaluateFast(). @rbac/rbac is around 4 KB, allows role updates at runtime, and ships Express, NestJS, and Fastify middleware. Neither supports attribute conditions, so neither appears in the ABAC scenarios. Both size figures are bundlephobia numbers checked 2026-03-30.

When to pick duck-iam

  • You want compile-time errors on misspelled action, resource, role, and scope names.
  • You need RBAC and ABAC together and do not want to maintain two policy systems that combine by hand.
  • You need multi-tenant scoped roles: the same subject holding different roles per organization.
  • You need to answer "why was this denied?" in production support, and explain() gives you the rule, the condition, and the actual versus expected values.
  • Your policies change at runtime, written by an admin UI or promoted between environments through engine.admin.export().
  • You want the operability surface (health probes, cache statistics, cross-instance invalidation, adapter timeouts) without building it.

When to pick something else

  • OPA or Cedar when policies must be enforced from more than one language. duck-iam is TypeScript only, and a shared policy set across a Go and a Python service is exactly what a DSL sidecar is for.
  • CASL when your rules are static, your bundle budget is tight, or your main need is scoping ORM queries. CASL has mature TypeORM, Sequelize, and Mongoose integrations, ships around 6 KB, and is faster per check because it compiles at build() time.
  • Casbin when you want a long-established engine with adapters for practically every database and language, and stringly-typed models are acceptable.
  • A hosted policy service such as Permit.io or Cerbos Cloud when you would rather not run a policy runtime at all and want a managed editing UI.
  • Nothing at all when your app has three roles and no conditions. A switch statement is a legitimate answer, and adding a policy engine to it is a cost with no return.

What duck-iam is not

  • Not authentication. It answers what a subject may do, never who they are. Pair it with duck-auth.
  • Not an audit log or SIEM. The hooks are first-class and every decision can be observed, but you bring the sink.
  • Not a permission taxonomy. There is no built-in manage:user vocabulary; you declare your own actions and resources.
  • Not a policy editor. The admin API and JSON schema are there so you can build one; the UI is yours.
  • Not language-agnostic. The policy format is JSON and portable, but the evaluator is TypeScript.

Gotchas

  • The compiled table addresses roles with a 32-bit mask, so a catalog over 32 roles cannot be compiled. The engine warns once and falls back to the interpreter in both modes: verdicts unchanged, the O(1) lookup gone. A real limit that no feature table would show.
  • Competitor versions above are the ones the benchmark suite pins. A newer release of any of them can move the numbers, so re-run bun run bench rather than trusting the table's age.

See also