@gentleduck/iam
v5.1.0
Published
Modern ABAC/RBAC access control engine. Framework-agnostic core with integrations for Express, NestJS, Hono, Next.js, React, and Vue.
Maintainers
Readme
Type-safe authorization engine for TypeScript. RBAC + ABAC with a policy engine, condition evaluation, scoped roles, and integrations for Express, NestJS, Hono, Next.js, React, Vue, and vanilla JS.
Zero runtime dependencies. Tree-shakeable. 23 KB full, under 1 KB per module.
Install
npm install @gentleduck/iam
# or
bun add @gentleduck/iamQuick start
import { createIam } from '@gentleduck/iam/core'
import { MemoryAdapter } from '@gentleduck/iam/adapters/memory'
const access = createIam({
actions: ['create', 'read', 'update', 'delete'] as const,
resources: ['post', 'comment', 'user'] as const,
roles: ['viewer', 'editor', 'admin'] as const,
})
const viewer = access.defineRole('viewer').grant('read', 'post').grant('read', 'comment').build()
const editor = access.defineRole('editor').inherits('viewer').grant('update', 'post').build()
const admin = access.defineRole('admin').inherits('editor').grantCRUD('post').grantCRUD('comment').build()
const policy = access
.policy('blog')
.rule('owner-edit', (r) => r.allow().on('update').of('post').when((w) => w.isOwner()))
.build()
const adapter = new MemoryAdapter({
policies: [policy],
roles: [viewer, editor, admin],
assignments: { 'user-1': ['editor'] },
})
const engine = access.createEngine({ adapter })
const allowed = await engine.can('user-1', 'read', { type: 'post', attributes: {} })
// truePerformance
Benchmarked against 7 JS authorization libraries using vitest bench. Simple RBAC check, ops/sec (higher is better):
| Library | ops/sec | vs CASL | |---------|---------|---------| | @casl/ability | 16,857,000 | baseline | | @gentleduck/iam [PROD] | 8,233,000 | 2x slower | | easy-rbac | 5,003,000 | 3.4x slower | | @rbac/rbac | 2,884,000 | 5.8x slower | | accesscontrol | 674,000 | 25x slower | | casbin | 143,000 | 118x slower | | role-acl | 140,000 | 120x slower |
CASL is faster on raw lookups because it pre-compiles rules into a hash table at build time. duck-iam supports dynamic policies that can change at runtime, which costs an extra Map lookup per check.
For the smallest bundle, import only what you use via subpaths:
// Engine-only (skip adapters, server middleware, client wrappers)
import { IamEngine, evaluatePolicyFast } from '@gentleduck/iam/core'
// Each adapter, server adapter, and client wrapper is a separate entry
import { MemoryAdapter } from '@gentleduck/iam/adapters/memory'
import { adminRouter } from '@gentleduck/iam/server/express'
import { useAccess } from '@gentleduck/iam/client/react'
// Validator (12 KB) - lazy-loaded by engine.admin.savePolicy on first
// call, or imported directly for standalone validation tooling
import { validatePolicy } from '@gentleduck/iam/core/validate'
// Fluent builder (9 KB) - config-time only, separate subpath
import { policy, defineRole } from '@gentleduck/iam/core/builder'import * from '@gentleduck/iam' pulls the everything-barrel (~41 KB
gzipped). Real deployments using subpath imports + tree-shaking come in
at 15-25 KB.
Features
- RBAC + ABAC combined in one engine
- Policy engine with 4 intra-policy algorithms (deny-overrides, allow-overrides, first-match, highest-priority) and 3 cross-policy combine modes (and / allow-overrides / first-applicable)
- 18 condition operators (eq, neq, gt, lt, in, contains, starts_with, matches, exists, subset_of, and more)
- Scoped roles for multi-tenant systems
- Dev/prod mode: rich Decision objects in development, plain booleans in production
- Explain API: full evaluation trace showing exactly why a permission was granted or denied
- Lifecycle hooks:
beforeEvaluate,afterEvaluate,onDeny,onError,onPolicyError,onMetrics - Type-safe config: actions, resources, roles, and scopes are validated at compile time
SRE primitives
engine.preload()- warm cache at bootengine.healthCheck()-/healthz-ready probe with adapter latency + cache hit rateengine.stats.get()/engine.stats.reset()- cache hit / miss counters per cacheengine.cache.invalidate()/invalidatePolicies()/invalidateRoles(id?)/invalidateSubject(id)- targeted cache flushesengine.admin.export()/import(snapshot, { mode })- schema-versioned policy + role snapshots for env promotionengine.dispose()- release the cross-instance invalidator subscription on shutdownIConfig.adapterTimeoutMs-AbortController-driven timeout on every adapter read (default 5 s)IConfig.maxPolicies/maxRoles- load-time caps that fail closedIConfig.allowFailOpen- explicit opt-in required to combinemode: 'production'withdefaultEffect: 'allow'IConfig.invalidator- cross-instance cache-invalidation broadcastercreateRedisInvalidatorat@gentleduck/iam/invalidators/redis- pub/sub helper with self-echo filtercreateMetricsAggregatorat@gentleduck/iam/observability/metrics- p50 / p95 / p99 overonMetricsevents- HttpAdapter retry + per-request timeout + circuit breaker (retries, backoff, threshold, cooldown)
- Required
authorizecallback on every admin router (Express, Hono, Next, Nest)
Integrations
Server middleware
// Express
import { guard, adminRouter } from '@gentleduck/iam/server/express'
app.delete('/posts/:id', guard(engine, 'delete', 'post'), handler)
app.use('/admin', adminRouter(engine, { authorize: (req) => isAdmin(req) })(() => express.Router()))
// Hono
import { guard, bindAdminRouter } from '@gentleduck/iam/server/hono'
app.delete('/posts/:id', guard(engine, 'delete', 'post'), handler)
bindAdminRouter(adminApp, engine, { authorize: (c) => isAdmin(c) })
// NestJS
import { nestAccessGuard, Authorize, createAdminOperations } from '@gentleduck/iam/server/nest'
@Authorize({ action: 'delete', resource: 'post' })
// Next.js
import { withAccess, createAdminHandlers } from '@gentleduck/iam/server/next'
export const DELETE = withAccess(engine, 'delete', 'post', handler)Client libraries
// React
import { createAccessControl } from '@gentleduck/iam/client/react'
const { AccessProvider, useAccess, Can, Cannot } = createAccessControl(React)
// Vue
import { createVueAccess } from '@gentleduck/iam/client/vue'
const { useAccess, Can, Cannot } = createVueAccess(vue)
// Vanilla JS
import { AccessClient } from '@gentleduck/iam/client/vanilla'
const client = await AccessClient.fromServer('/api/permissions')
client.can('read', 'post') // booleanDatabase adapters
import { MemoryAdapter } from '@gentleduck/iam/adapters/memory'
import { FileAdapter } from '@gentleduck/iam/adapters/file'
import { PrismaAdapter } from '@gentleduck/iam/adapters/prisma'
import { DrizzleAdapter } from '@gentleduck/iam/adapters/drizzle'
import { RedisAdapter } from '@gentleduck/iam/adapters/redis'
import { HttpAdapter } from '@gentleduck/iam/adapters/http'Operability
import { createRedisInvalidator } from '@gentleduck/iam/invalidators/redis'
import { createMetricsAggregator } from '@gentleduck/iam/observability/metrics'
const metrics = createMetricsAggregator()
const engine = new IamEngine({
adapter,
invalidator: createRedisInvalidator({ client: redis }),
hooks: { onMetrics: metrics.record },
})
await engine.preload()
app.get('/healthz', async (_, res) => res.json(await engine.healthCheck()))
app.get('/metrics', (_, res) => res.json(metrics.snapshot()))See the production deployment guide for cache TTL trade-offs, multi-node invalidation patterns, fail-closed defaults, and SLO targets.
Module sizes (gzipped)
| Module | Size |
|--------|------|
| Core engine (typical import) | ~15 KB |
| core/validate (admin only, lazy-loaded) | 12 KB |
| core/builder (config-time only) | 9 KB |
| core/explain (dev-mode trace) | separate chunk |
| Each adapter | 1.7 - 6 KB |
| Each server middleware | 2.4 - 3.7 KB |
| Each client library | 1.2 - 2.0 KB |
The "full" bundle headline in benchmarks (~41 KB) is the worst-case
"import everything" number - what import * from '@gentleduck/iam'
would pull. Realistic deployments end up at 15-25 KB because adapters,
server middleware, and clients live behind subpath imports and the
validator is lazy-loaded only when admin write paths run. See the
benchmarks page
for per-profile measurements.
Docs
- Site: gentleduck.org/duck-iam
- Devtools: import
@gentleduck/iam/dtto inspect policy evaluation inside your app - Sibling repos:
@gentleduck/auth,@gentleduck/ui,@gentleduck/upload,@gentleduck/md
Contributing
PR checklist + style notes in the repo's CONTRIBUTING.md.
Security disclosures: SECURITY.md.
License
MIT. See LICENSE.
