How duck-iam compares
WIPWhere 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:
| Library | Version 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.
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.
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-iam | Casbin | CASL | Oso (docs) | OPA / Cedar (docs) | AccessControl.js | |
|---|---|---|---|---|---|---|
| Language | TypeScript | TS, Go, Python, and more | TypeScript | TS with a Rust core | Rego or Cedar DSL | TypeScript |
| RBAC | First-class | Yes, RBAC model | Yes | Yes | Yes | Yes |
| ABAC conditions | 19 operators | Yes, via matchers | Yes, MongoDB-style | Yes | Yes | No |
| One pipeline for both | Yes, roles compile to a policy | Two model files | One ability set | One pipeline | One DSL | RBAC only |
| Combining algorithms | 4 in-policy, 3 cross-policy | Custom matcher | Subject-side evaluation | Single policy | Written in the DSL | Not applicable |
| Multi-tenant scoping | First-class scoped roles | Manual, via subjects | Manual | Manual | Manual | No |
| Decision traces | engine.explain(), development mode | Limited | Limited | inspect() | OPA explain | No |
| Runtime policy reload | Yes, adapter-driven with an invalidator | Yes, watcher | Rebuild the ability | Yes | Yes | Rebuild the grants |
| Type inference | Const-typed actions, resources, roles, scopes | Stringly typed | Strong | Strong | None, it is a DSL | Stringly typed |
| Development and production modes | Yes | No | No | No | No | No |
| Server middleware shipped | Express, Hono, NestJS, Next.js, generic | Bring your own | Bring your own | Bring your own | Bring your own | Bring your own |
| Client packages shipped | React, Vue, vanilla | No | React Can component | No | No | No |
| Storage adapters shipped | Memory, file, Prisma, Drizzle, Redis, HTTP | 20+ across languages | No | No | No | No |
| Built-in caching | LRU with TTL, single-flighted, cross-instance invalidator | No | Not needed, rules are in memory | No | Bundle cache | No |
| Batch permission checks | engine.permissions() | No | No | No | Partial evaluation | No |
| Rule indexing | Per-policy Map, held in a WeakMap | No | Yes | Yes | Yes | No |
| Runtime dependencies | 1, uuid, only for the Drizzle schemas | 5 | 0 | native binding | separate binary | 1 |
| Measured here | Yes | Yes | Yes | No | No | Yes |
| License | MIT | Apache 2.0 | MIT | Apache 2.0 | Apache 2.0 | MIT |
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
switchstatement 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:uservocabulary; 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 benchrather than trusting the table's age.
See also
- Benchmarks: the measured numbers, the hardware, and the methodology.
- Introduction: the model and the docs routing map.
- Cookbook: the patterns this comparison keeps alluding to.
- Pairing with duck-auth: the identity half duck-iam deliberately does not do.