type safe config overview
createAccessConfig() — lock down actions, resources, scopes, roles, and conditions at the type level. Compile-time validation for the whole permission schema.
Two ways to define permissions
duck-iam provides two ways to define permissions:
| Approach | Best for |
|---|---|
Untyped builders — defineRole(), policy(), defineRule(), when() from the root | Quick prototypes, scripts, library code |
Typed config — createAccessConfig() | Production apps, anywhere typos cost real money |
For production applications, use the typed config. It prevents an entire class of bugs where a permission check silently fails because of a typo in an action or resource name.
Reading order
| Page | Covers |
|---|---|
| createAccessConfig | Factory, input shape, optional fields |
| typed context | Per-resource attribute narrowing, DotPaths, value autocomplete |
$-references with types | DollarPaths autocomplete on values |
| methods reference | defineRole, policy, defineRule, when, createEngine, checks, validateRoles, validatePolicy |
| comparison | Typed vs untyped — what each catches, what each doesn't |
Typed vs untyped at a glance
Untyped (direct imports)
import { defineRole, Engine, MemoryAdapter } from '@gentleduck/iam'
const viewer = defineRole('viewer')
.grant('raed', 'post') // typo: "raed" instead of "read" — NO error
.build()
const engine = new Engine({ adapter })
await engine.can('user-1', 'raed', { type: 'post', attributes: {} })
// No TypeScript error, but silently fails at runtime because no role grants "raed"import { defineRole, Engine, MemoryAdapter } from '@gentleduck/iam'
const viewer = defineRole('viewer')
.grant('raed', 'post') // typo: "raed" instead of "read" — NO error
.build()
const engine = new Engine({ adapter })
await engine.can('user-1', 'raed', { type: 'post', attributes: {} })
// No TypeScript error, but silently fails at runtime because no role grants "raed"Typed (createAccessConfig)
import { createAccessConfig } from '@gentleduck/iam'
const access = createAccessConfig({
actions: ['create', 'read', 'update', 'delete'] as const,
resources: ['post', 'comment'] as const,
})
const viewer = access
.defineRole('viewer')
.grant('raed', 'post') // ERROR: '"raed"' is not assignable to '"create" | "read" | ...'
.build()import { createAccessConfig } from '@gentleduck/iam'
const access = createAccessConfig({
actions: ['create', 'read', 'update', 'delete'] as const,
resources: ['post', 'comment'] as const,
})
const viewer = access
.defineRole('viewer')
.grant('raed', 'post') // ERROR: '"raed"' is not assignable to '"create" | "read" | ...'
.build()The typed version catches the typo immediately. For any application with more than a handful of permissions, this prevents real bugs.
Quick start
import { createAccessConfig } from '@gentleduck/iam'
const access = createAccessConfig({
actions: ['create', 'read', 'update', 'delete', 'manage'] as const,
resources: ['post', 'comment', 'user', 'dashboard'] as const,
scopes: ['org-1', 'org-2'] as const,
roles: ['viewer', 'editor', 'admin'] as const,
})
// Typed builders — invalid actions/resources/roles are compile errors
const viewer = access.defineRole('viewer').grant('read', 'post').build()
const engine = access.createEngine({ adapter })
await engine.can('user-1', 'read', { type: 'post', attributes: {} })import { createAccessConfig } from '@gentleduck/iam'
const access = createAccessConfig({
actions: ['create', 'read', 'update', 'delete', 'manage'] as const,
resources: ['post', 'comment', 'user', 'dashboard'] as const,
scopes: ['org-1', 'org-2'] as const,
roles: ['viewer', 'editor', 'admin'] as const,
})
// Typed builders — invalid actions/resources/roles are compile errors
const viewer = access.defineRole('viewer').grant('read', 'post').build()
const engine = access.createEngine({ adapter })
await engine.can('user-1', 'read', { type: 'post', attributes: {} })as const is required. Without it, TypeScript widens the arrays to string[] and you lose all type checking.
When to use each
| Scenario | Recommendation |
|---|---|
| Production application | Use createAccessConfig() for type safety |
| Quick prototype or spike | Untyped imports are faster to set up |
| Dynamic permissions from DB | Use untyped for the dynamic parts, validate with validatePolicy() |
| Library or framework code | Use generic type parameters for maximum flexibility |
| Testing | Either works; typed catches config mistakes, untyped is less verbose |