`createIam()` is the only function in duck-iam that takes your permission vocabulary and returns builders bound to it. It is a pure factory: it allocates one object literal, stores four arrays, and closes over the builder constructors. This page documents its input, its generics, and its return value against `src/core/config/config.ts` and `src/core/config/config.types.ts`.

## Signature

```ts
export function createIam<
  const TActions extends readonly string[],
  const TResources extends readonly string[],
  const TRoles extends readonly string[] = readonly string[],
  const TScopes extends readonly string[] = readonly string[],
  TContext extends object = DotPath.IDefaultContext,
>(
  input: IamConfig.IAccessConfigInput<TActions, TResources, TRoles, TScopes, TContext>,
): IamConfig.IAccessConfig<
  TActions[number],
  TResources[number],
  TRoles[number],
  TScopes[number],
  TContext
>
```

```ts
import { createIam } from '@gentleduck/iam'

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', 'super-admin'] as const,
})
```

## Options

Every field of `IamConfig.IAccessConfigInput`.

| Option | Type | Required | Default | What it constrains |
|---|---|---|---|---|
| `actions` | `TActions extends readonly string[]` | yes | none | `grant(action, ...)`, `grantWhen(action, ...)`, `rule.on(...)`, `engine.can(_, action, ...)`, entries in `checks()` |
| `resources` | `TResources extends readonly string[]` | yes | none | `grant(_, resource)`, `rule.of(...)`, `when().resourceType(...)`, the `type` field of a resource passed to the engine |
| `scopes` | `TScopes extends readonly string[]` | no | `[]` on the returned object; the type parameter defaults to `readonly string[]` | The third argument of `grant`, `role.scope(...)`, `when().scope(...)` / `when().scopes(...)`, the `scope` argument of engine checks |
| `roles` | `TRoles extends readonly string[]` | no | `[]` on the returned object; the type parameter defaults to `readonly string[]` | `defineRole(id)`, `inherits(...)`, `when().role(...)` / `when().roles(...)`, `policy.targets.roles` |
| `context` | `TContext extends object` | no | `DotPath.IDefaultContext` | Field paths and value types on `when().check/eq/neq/in/attr/resourceAttr/env` - see [typed context](/duck-iam/advanced/config/context) |

The runtime value of `context` is read by nothing. It exists so TypeScript can capture `TContext` from an argument position. Give it a value that costs nothing at runtime, such as `{} as unknown as AppContext`; the object is discarded and only the type survives.

## How the generics resolve

The four vocabulary parameters are declared `const`, so TypeScript preserves the literal tuple at the call site and the factory indexes it with `[number]` to get a union.

The two branches are the whole story. Once an array has widened to `string[]` at its declaration site, nothing later in the pipeline can recover the literals, and every builder silently accepts any string. If you want the arrays in a separate module, annotate them there:

```ts
// vocabulary.ts
export const ACTIONS = ['create', 'read', 'update', 'delete'] as const
export const RESOURCES = ['post', 'comment'] as const

// access.ts
import { createIam } from '@gentleduck/iam'
import { ACTIONS, RESOURCES } from './vocabulary'

export const access = createIam({ actions: ACTIONS, resources: RESOURCES })
```

## What you get back

`IamConfig.IAccessConfig` has four readonly properties and eight methods.

| Member | Type | Notes |
|---|---|---|
| `actions` | `readonly TAction[]` | The array you passed, by reference |
| `resources` | `readonly TResource[]` | The array you passed, by reference |
| `scopes` | `readonly TScope[]` | `input.scopes ?? []` |
| `roles` | `readonly TRole[]` | `input.roles ?? []` |
| `defineRole(id)` | `RoleBuilder

The two branches per option are independent, which the `scopes/roles default to [] independently of one another` test pins directly. Concretely:

```ts
const access = createIam({
  actions: ['read', 'write'] as const,
  resources: ['post'] as const,
})

access.defineRole('anything-at-all')          // compiles: TRole is string
access.defineRole('viewer').grant('read', 'post', 'any-scope') // compiles: TScope is string
access.scopes // []
access.roles  // []
```

Declaring `roles` flips `defineRole`, `inherits`, `when().role()`, and `when().roles()` to the declared union. Declaring `scopes` flips the scope argument of `grant`, `role.scope()`, `when().scope()`, and `when().scopes()`. Declaring `context` is what turns on dot-path autocomplete; without it the builders accept any string field path and `IamPrimitives.AttributeValue` for values, and runtime behaviour is unchanged.

## Deriving the unions elsewhere

The config exposes its vocabulary as readonly arrays, so you can index them for the union type instead of restating it:

```ts
export const access = createIam({
  actions: ['create', 'read', 'update', 'delete'] as const,
  resources: ['post', 'comment'] as const,
  roles: ['viewer', 'editor'] as const,
})

export type AppAction = (typeof access.actions)[number]     // 'create' | 'read' | 'update' | 'delete'
export type AppResource = (typeof access.resources)[number] // 'post' | 'comment'
export type AppRole = (typeof access.roles)[number]         // 'viewer' | 'editor'
```

Those aliases are what you pass to `IamMemoryAdapter`, `IamDrizzleAdapter`, or any server wrapper so the adapter's generics line up with the config's.

## Gotchas

* **`defineRule` has a different generic order.** It is `RuleBuilder<TAction, TResource, TScope, TRole, TContext>` while `defineRole`, `definePolicy`, and `when` are `<TAction, TResource, TRole, TScope, TContext>`. This only bites when you write an explicit type annotation for a rule builder; the factory wires it correctly for you.
* **`validateRoles` takes the *unconstrained* `IRole`, on purpose.** A runtime validator exists for data whose type you do not trust, and a signature narrowed to the declared unions could only accept values already proven correct. Roles read from a database go through this method directly - and this one, unlike the bare export, also flags a grant naming vocabulary the config never declared (`UNREACHABLE_TARGET`).
* **`createEngine` does not infer `TMode` from `config.mode`.** `mode` is optional, so the type argument and the config field move independently. Pass both or neither.
* **`createIam` pulls the validator into your bundle.** `core/index.ts` deliberately does not re-export the validator functions so the roughly 12 KB chunk stays opt-in, but `config.ts` imports `validatePolicy` and `validateRoles` directly and the returned object closes over both. If you use `createIam`, that code is reachable whether or not you call the validate methods.
* **`as const` on the outer object does nothing useful.** The `const` type parameters already capture the tuples. Annotating each array is what matters.

## See also

* [Methods reference](/duck-iam/advanced/config/methods) - each returned method with its signature and behaviour.
* [Typed context](/duck-iam/advanced/config/context) - what the `context` option gives you.
* [Typed vs untyped](/duck-iam/advanced/config/comparison) - whether the setup is worth it for your project.
* [Types and namespaces](/duck-iam/types) - `IamConfig.IAccessConfigInput` and `IamConfig.IAccessConfig` in the wider type map.