## What is a PermissionMap?

A `PermissionMap` is a flat object - keys encode the permission, values are booleans. It is the wire format every client library consumes.

```typescript
{
  // Format: "action:resource"
  "create:post": true,
  "delete:post": true,
  "manage:team": false,

  // Format: "action:resource:resourceId"
  "delete:post:abc123": false,

  // Format: "scope:action:resource"
  "org-1:manage:billing": true,

  // Format: "scope:action:resource:resourceId"
  "org-1:update:post:post-42": true,
}
```

Generate it on the server with `engine.permissions()` or the `getPermissions` helper from `@gentleduck/iam/server/next`. Missing keys return `false` (deny by default).

***

## Key formats

| Shape | Format | Example |
| --- | --- | --- |
| Action + resource | `action:resource` | `create:post` |
| Action + resource + id | `action:resource:resourceId` | `delete:post:abc123` |
| Scope + action + resource | `scope:action:resource` | `org-1:manage:billing` |
| Full | `scope:action:resource:resourceId` | `org-1:update:post:post-42` |

The shape is determined by which fields you pass when generating the map (or calling `can()` on the client). The client-side `can()` uses the **same encoding** - pass a `resourceId` or `scope` only when the server included one.

***

## iamBuildPermissionKey

The same key builder powers every client integration:

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

iamBuildPermissionKey('delete', 'post')
// "delete:post"

iamBuildPermissionKey('update', 'post', 'post-42', 'org-1')
// "org-1:update:post:post-42"
```

Order: `(action, resource, resourceId?, scope?)`. The serialized key always reorders as `scope:action:resource:resourceId` so server and client agree.

You rarely call `iamBuildPermissionKey()` directly - the React, Vue, and vanilla clients call it internally. Reach for it when:

* Writing tests that assert specific keys
* Building custom permission map endpoints
* Inspecting maps in dev tools

***

## Generating a map (server side)

```typescript
import { engine } from '@/lib/engine'

const permissions = await engine.permissions('user-1', [
  { action: 'create', resource: 'post' },
  { action: 'delete', resource: 'post', resourceId: 'post-42' },
  { action: 'manage', resource: 'billing', scope: 'org-1' },
])
```

Result keyed exactly as listed above:

```jsonc
{
  "create:post": true,
  "delete:post:post-42": false,
  "org-1:manage:billing": true
}
```

***

## Reading a map (client side)

The shape of your `can(...)` call must match the shape of the key the server generated. Forgetting `scope` or `resourceId` results in a missed key and a `false` return.

```typescript
client.can('create', 'post') // matches "create:post"
client.can('delete', 'post', 'post-42') // matches "delete:post:post-42"
client.can('manage', 'billing', undefined, 'org-1') // matches "org-1:manage:billing"
```

The clients fail closed: if the key is missing, `can` returns `false`.

***

## Stale map handling

Permissions change when:

* A user is granted/revoked a role
* A scope changes (joined/left an org)
* A feature flag flips
* An attribute changes (e.g. account verification, plan tier)

The client cannot detect these - refresh the map from the server when you know one happened. See [refreshing permissions](/duck-iam/integrations/client) in the client overview.

***

## Notes & caveats

* **Don't mix scopes in one map.** Generate per-scope maps when scope materially changes the answer; mixing creates ambiguous keys for "global" lookups.
* **`resourceId` keys are dense.** Don't pre-generate every possible `(action, resource, id)` combination - generate them lazily for visible records.
* **Client-side maps are not a security boundary.** They drive UI gates only. Always re-check on the server when the action mutates state.