Skip to main content

Type-safe config overview

createIam() turns as const arrays of actions, resources, scopes and roles into union types that constrain every builder in the package

createIam() is the typed entry point of duck-iam. You hand it the vocabulary of your application - the actions, resource types, scopes, and role IDs it supports - and it hands back builders whose every string argument is constrained to that vocabulary. This page explains the two ways to define permissions and points at the reference pages for each part of the API.

Two ways to define permissions

Both paths produce identical runtime data. The difference is entirely at compile time.

ApproachImportBest for
Untyped buildersdefineRole, definePolicy, defineRule, when from @gentleduck/iamPrototypes, scripts, library code that must stay generic
Typed configcreateIam from @gentleduck/iamProduction apps, anywhere a typo in a permission string costs money

How the types flow

This shows what createIam() does with the arrays you pass it.

Loading diagram...

The const type parameters on createIam (const TActions extends readonly string[]) mean TypeScript keeps the literal tuple even without an explicit as const in many positions - but a plain const actions = [...] declared elsewhere and then passed in has already widened to string[], and nothing can recover the literals after that. Declare the arrays inline, or annotate them as const at their declaration site.

What the config object holds

The return value is a plain object literal: four readonly arrays and eight methods. There is no class and no hidden state.

Loading diagram...

actions and resources are passed straight through from the input. scopes and roles fall back to [] when you omit them. The four builder methods each construct a fresh builder instance per call; checks is an identity function; validatePolicy forwards to the standalone validator unchanged, and validateRoles forwards the declared actions / resources / scopes along with the roles, so it additionally flags a grant naming vocabulary this config never declared.

Reading order

PageCovers
createIam()Full options table, generic inference, optional-field behaviour, return shape
Methods referenceEvery method on the returned object with its verified signature
Typed contextHow context drives dot-path autocomplete and per-resource attribute narrowing
Typed $-referencesDollarPaths, where $ values are accepted, and how they resolve at evaluation time
Typed vs untypedAn honest account of what each approach catches and what neither catches

Quick start

import { createIam } from '@gentleduck/iam'
import { IamMemoryAdapter } from '@gentleduck/iam/adapters/memory'

const access = createIam({
  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,
})

const viewer = access.defineRole('viewer').grant('read', 'post').build()

const adapter = new IamMemoryAdapter<
  'create' | 'read' | 'update' | 'delete' | 'manage',
  'post' | 'comment' | 'user' | 'dashboard',
  'viewer' | 'editor' | 'admin',
  'org-1' | 'org-2'
>({ roles: [viewer], assignments: { 'user-1': ['viewer'] } })

const engine = access.createEngine({ adapter })

await engine.can('user-1', 'read', { type: 'post', attributes: {} })

The adapter's generic arguments are <TAction, TResource, TRole, TScope> and must line up with the config's unions, because createEngine expects an IamAdapter.IAdapter in exactly those parameters. Constructing the adapter with explicit generics, as above, is what makes that check meaningful.

When to use each

ScenarioRecommendation
Production applicationcreateIam() - the compile errors pay for the setup within a week
Prototype or one-off scriptUntyped builders; less ceremony, same runtime
Permissions loaded from a database at runtimeUntyped for the dynamic half, then validatePolicy() at the boundary
Library or framework codeKeep the generic parameters open; do not bake a vocabulary into a reusable package
TestsEither; typed catches fixture drift, untyped is shorter

See also