## Two ways to define permissions

duck-iam provides two ways to define permissions:

| Approach | Best for |
| --- | --- |
| **Untyped builders** - `defineRole()`, `definePolicy()`, `defineRule()`, `when()` from the root | Quick prototypes, scripts, library code |
| **Typed config** - `defineIam()` | 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 |
| --- | --- |
| [defineIam](/duck-iam/advanced/config/access-config) | Factory, input shape, optional fields |
| [typed context](/duck-iam/advanced/config/context) | Per-resource attribute narrowing, `DotPaths`, value autocomplete |
| [`$`-references with types](/duck-iam/advanced/config/dollar-paths) | `DollarPaths` autocomplete on values |
| [methods reference](/duck-iam/advanced/config/methods) | `defineRole`, `policy`, `defineRule`, `when`, `createEngine`, `checks`, `validateRoles`, `validatePolicy` |
| [comparison](/duck-iam/advanced/config/comparison) | Typed vs untyped - what each catches, what each doesn't |

***

## Typed vs untyped at a glance

### Untyped (direct imports)

```typescript
import { defineRole, IamEngine, IamMemoryAdapter } from '@gentleduck/iam'

const viewer = defineRole('viewer')
  .grant('raed', 'post') // typo: "raed" instead of "read" - NO error
  .build()

const engine = new IamEngine({ adapter })
await engine.can('user-1', 'raed', { type: 'post', attributes: {} })
// No TypeScript error, but silently fails at runtime because no role grants "raed"
```

### Typed (defineIam)

```typescript
import { defineIam } from '@gentleduck/iam'

const access = defineIam({
  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

```typescript
import { defineIam } from '@gentleduck/iam'

const access = defineIam({
  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 `defineIam()` 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 |