Skip to main content

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:

Loading diagram...

ApproachBest for
Untyped buildersdefineRole(), policy(), defineRule(), when() from the rootQuick prototypes, scripts, library code
Typed configcreateAccessConfig()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

PageCovers
createAccessConfigFactory, input shape, optional fields
typed contextPer-resource attribute narrowing, DotPaths, value autocomplete
$-references with typesDollarPaths autocomplete on values
methods referencedefineRole, policy, defineRule, when, createEngine, checks, validateRoles, validatePolicy
comparisonTyped 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

ScenarioRecommendation
Production applicationUse createAccessConfig() for type safety
Quick prototype or spikeUntyped imports are faster to set up
Dynamic permissions from DBUse untyped for the dynamic parts, validate with validatePolicy()
Library or framework codeUse generic type parameters for maximum flexibility
TestingEither works; typed catches config mistakes, untyped is less verbose