# Resources and catalog (/docs/concepts/resources-and-catalog)

> The vocabulary policies speak: actions, resource types, application-owned and managed resources, relations, and tenant-defined types.



Authorization in Better IAM always asks the same question: may this principal (the
signed-in caller) perform this action on this resource? The
resource catalog defines both halves: it lists the action names a policy may use and the resource types those
actions apply to, with their attributes and relations.

Declaring them up front means a typo in a policy is caught when the policy is saved, not discovered later when
access silently fails. This page explains the concepts; the [catalog guide](/docs/authorization/catalog)
covers configuration and validation in depth.

## The permission catalog [#the-permission-catalog]

The permission catalog is the set of action names a
policy may use. It combines four sources:

| Source                 | Example                                                          | Where it comes from                                                   |
| ---------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------- |
| Built-in actions       | `iam:identities:create`, `iam:bindings:create`, `iam:audit:read` | The platform's own operations.                                        |
| Product actions        | `documents:read`, `reports:export`                               | `permissions.actions` and each resource type's `actions`.             |
| Plugin actions         | `projects:read`                                                  | A plugin's `actions` list.                                            |
| Tenant-defined actions | `invoice:approve`                                                | Registered by a tenant when `permissions.mode` is `'tenant-defined'`. |

Registering an action grants nothing; it only makes the name valid in policies. Policy documents are validated
against the catalog when they are stored: an exact action name that does not exist is rejected with
`INVALID_ACTION`. Wildcard patterns such as `documents:*` are stored as written. Products and plugins cannot
declare names under the reserved `iam:` or `tenant/` prefixes, or names containing `*`, `?`, or whitespace.

## Resource types [#resource-types]

A resource type names a kind of thing your product protects, such as documents or
projects, and says which actions apply to it and which attributes policies may test. You declare types in
`permissions.resourceTypes`:

```ts title="iam.ts"
export const iam = betterIam({
  // ...
  permissions: {
    resourceTypes: {
      document: {
        actions: ['documents:read', 'documents:write'],
        attributes: { ownerId: 'string', classification: 'string' },
        relations: ['viewer', 'editor'],
      },
      project: {
        managed: true,
        actions: ['projects:read', 'projects:manage'],
        attributes: { archived: 'boolean' },
      },
      task: { managed: true, parent: 'project', actions: ['tasks:read', 'tasks:write'] },
    },
  },
});
```

<TypeTable
  type="{
  actions: {
    type: 'string[]',
    description: <>Fully qualified action names that apply to this type. They join the permission catalog.</>,
  },
  attributes: {
    type: &#x22;Record<string, 'string' | 'number' | 'boolean'>&#x22;,
    description: <>The attribute schema, at most 64 attributes. Policies read the values as <code>{'resource.{name}'}</code>.</>,
  },
  relations: {
    type: 'string[]',
    description: <>Relation names (such as <code>viewer</code> or <code>editor</code>) identities and groups may hold on resources of this type. At most 32 lowercase names.</>,
  },
  parent: {
    type: 'string',
    description: <>The parent resource type, for hierarchical resources. A managed type needs a managed parent.</>,
  },
  managed: {
    type: 'boolean',
    default: 'false',
    description: <>Managed types are registered with IAM and resolved from its registry instead of <code>resolveResource</code>.</>,
  },
  description: {
    type: 'string',
    description: <>A human-readable description, at most 512 characters.</>,
  },
}"
/>

Type names are lowercase identifiers (a letter, then letters, digits, or hyphens, up to 64 characters). A small
set of names is reserved for the platform: `iam`, `role`, `oauth-client`, `scim`, `saml`, `ssf`, `tenant`,
`identity`, and `session`. Declaring a reserved name, or the same name twice (including across plugins), fails at
construction with `INVALID_CONFIG`.

Resource patterns in policies match `type/id` within an already-resolved tenant, for example `document/*` or
`folder/home-${principal.id}`. Once `resourceTypes` is configured, an exact type in a pattern must exist
(`INVALID_RESOURCE_TYPE`).

## Application-owned and managed resources [#application-owned-and-managed-resources]

Every resource type is either application-owned or managed. The difference is where Better IAM finds a
resource's tenant, owner, and attributes at decision time.

|                 | Application-owned                                             | Managed                                                        |
| --------------- | ------------------------------------------------------------- | -------------------------------------------------------------- |
| Declared as     | A type without `managed`                                      | `managed: true`, and every tenant-defined type                 |
| Records live in | Your product's database                                       | IAM's `resources` collection                                   |
| Resolved by     | Your `resolveResource` callback                               | The registry; no callback                                      |
| Registered with | Nothing; your product owns the data                           | The `resources` group (see below)                              |
| Extra context   | Whatever your resolver returns                                | `resource.ownerId`, `resource.parentId`, `resource.parentType` |
| Reverse queries | Not enumerable; use `authorizeMany` with IDs you already have | `listAccessible` lists what a caller may act on                |

For application-owned types, `resolveResource` must load ownership and attributes from trusted storage:

```ts title="iam.ts"
resolveResource: async ({ tenantId, type, id }) => {
  const document = await documents.find(id); // your database
  return { tenantId: document.tenantId, type, id, attributes: { ownerId: document.ownerId } };
},
```

> **Resolve from trusted storage.** 
  Never copy the request's tenant ID into a fetched record to satisfy the resolver. The tenant the resolver
  returns is what authorization checks the caller against.

The resolver must return the same `type` and `id` it was asked about, and the record's tenant must be the tenant
the check names; anything else fails with `RESOURCE_MISMATCH` (403). Without a `resolveResource` option, checks on
application-owned types fail with `RESOURCE_RESOLVER_REQUIRED`.

Choose managed resources when you would rather not write a resolver, or when you need
reverse queries such as "which projects may this person open?". You then tell
Better IAM about each resource with the `resources` group:

* `resources.register` records one resource with its attributes, an optional owner (`ownerId`), and an optional
  parent (`parentId`), for example when a project is created in your product.
* `resources.registerMany` records up to 100 in one transaction, for imports. A denied item rejects the batch.
* `resources.update` changes a resource's attributes or owner, and `resources.delete` removes it, together with
  its relationship tuples, when the product deletes it.
* `resources.get` reads one registration, and `resources.list` lists them by type, parent, or owner.

```ts
await iam.api.resources.register(credential, {
  tenantId,
  type: 'project',
  id: project.id,
  ownerId: creatorIdentityId,
  attributes: { archived: false },
});
```

**Route:** `POST /resources/register` (also `registerMany`, `update`, `delete`, `get`, `list` under `/resources/`)

Attribute values are validated against the declared schema, `ownerId` must be an identity of the same tenant, and
a parent must be a registered resource of the declared parent type. Registering requires `iam:resources:create` on
`iam/{type}/{id}`, so policies can scope who may register which resources. Managed registrations are trusted
authorization inputs: the `iam:resources:*` permissions decide who may create and edit them.

## Relations and relationship tuples [#relations-and-relationship-tuples]

Roles answer "what can editors do?", but sharing needs "who may edit **this** document?". Writing a policy per
document does not scale. Relations express sharing and ownership without listing resource IDs in policies.

A relationship tuple records that an identity or a group stands in a named relation
to one resource: &#x2A;alice is an `editor` of `document/plans`*, &#x2A;the design group are `viewer`s of `folder/specs`*.
Only relations the resource type declares are accepted.

* `relationships.create` adds a tuple, optionally with an `expiresAt`, for example when someone shares a document.
* `relationships.list` finds tuples by resource, relation, or subject, for a "shared with" panel.
* `relationships.delete` removes one, when access is unshared.

**Route:** `POST /relationships/create` (also `POST /relationships/list`, `POST /relationships/delete`)

During evaluation, the principal's live relations on the resource (held directly or through a group) appear as
`resource.relations`, and those on its registered parent as `resource.parentRelations`:

```ts
{
  effect: 'allow',
  actions: ['documents:read'],
  resources: ['document/*'],
  conditions: { ArrayContains: { 'resource.relations': ['viewer', 'editor'] } },
}
```

A relation still held by someone cannot be dropped from its type. See
[relationships](/docs/authorization/relationships).

## Identity attributes [#identity-attributes]

Policies often depend on facts about the person rather than the resource: their department, clearance level, or
region. `permissions.identityAttributes` declares these as typed attributes. Administrators set them with
`identities.update` (or `serviceAccounts.update`), federation and SCIM can fill them from a directory, and policies
read them as `principal.{name}`:

```ts
permissions: {
  identityAttributes: { department: 'string', clearance: 'number' },
},
```

Attribute names cannot shadow the principal keys the server derives itself, such as `id`, `tenantId`, `mfa`,
`mfaTime`, `kind`, `owner`, `rootAdmin`, `groups`, `roles`, `sessionId`, `sessionKind`, `authMethod`,
`impersonated`, and `impersonatorId`, so an attribute can never impersonate a fact the server vouches for.

## Tenant-defined resource types [#tenant-defined-resource-types]

Platforms whose customers model their own data, such as a workflow or low-code product, cannot declare every
resource type in advance. With `permissions.mode: 'tenant-defined'`, each tenant can extend the catalog for itself.
An organization owner registers a type, with its actions, attributes, and relations, using
`resourceTypes.register`:

```ts
await iam.api.resourceTypes.register(ownerCredential, {
  tenantId,
  name: 'invoice',
  actions: ['read', 'approve'], // registered as invoice:read and invoice:approve
  attributes: { amount: 'number' },
  relations: ['approver'],
});
```

**Route:** `POST /resourceTypes/register` (actions: `POST /actions/register`, `POST /actions/list`, `POST /actions/unregister`)

* Tenant-defined types are always managed, and exist only in the tenant that registered them.
* Their actions are namespaced under the type as `{type}:{verb}`. The `actions` list on `resourceTypes.register`
  creates them, `actions.register` adds one later, and `actions.list` shows the whole catalog a tenant can use.
* `resourceTypes.update` changes a type's description, attributes, or relations and adds verbs;
  `resourceTypes.delete` and `actions.unregister` remove what is no longer used.
* Names cannot collide with reserved names, platform resource types, or the namespace of any platform action
  (`INVALID_RESOURCE_TYPE`, `INVALID_ACTION`), so tenant-created actions cannot redefine platform namespaces.
* Deleting a type or action is refused while resources, relationships, child types, or policies still use it
  (`RESOURCE_IN_USE`).
* In the default `'catalog'` mode, only developer-defined names are allowed and these calls fail with
  `CATALOG_LOCKED`.

## From request to decision [#from-request-to-decision]

<Mermaid
  chart="flowchart LR
  R[&#x22;Request<br/>action + type/id&#x22;] --> K{Known action?}
  K -- no --> X[INVALID_ACTION]
  K -- yes --> M{Managed type?}
  M -- yes --> G[&#x22;Registry<br/>attributes, owner, parent&#x22;]
  M -- no --> A[&#x22;resolveResource<br/>your trusted storage&#x22;]
  G --> C[&#x22;Context<br/>resource.*, relations&#x22;]
  A --> C
  C --> E[Policy evaluation]"
/>

Whichever way a resource is resolved, its attributes, owner, parent, and relations become `resource.*` context
keys, and the policy engine evaluates the principal's roles and policies against them. Continue with
[policies](/docs/authorization/policies) and [conditions](/docs/authorization/conditions).

## Options [#options]

| Option                           | Type                                                           | Default     | Description                                                                                                  |
| -------------------------------- | -------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------ |
| `permissions.mode`               | `'catalog' \| 'tenant-defined'`                                | `'catalog'` | `'tenant-defined'` lets each tenant register its own resource types and actions.                             |
| `permissions.actions`            | `string[]`                                                     | `[]`        | Product actions not tied to a resource type.                                                                 |
| `permissions.resourceTypes`      | `Record<string, ResourceTypeDefinition>`                       | `{}`        | Platform resource types with `actions`, `attributes`, `relations`, `parent`, `managed`, and `description`.   |
| `permissions.identityAttributes` | `Record<string, 'string' \| 'number' \| 'boolean'>`            | `{}`        | Typed identity attributes, read as `principal.{name}`.                                                       |
| `resolveResource`                | `(ref: ResourceRef) => Promise<ResourceRef & { attributes? }>` | none        | Loads application-owned resources from your trusted storage. Required for checks on application-owned types. |
| `plugins`                        | `IamPlugin[]`                                                  | `[]`        | Plugins add actions and resource types like `permissions` does ([extensions](/docs/concepts/extensions)).    |

Invalid or reserved names, duplicate types, and broken parent chains fail construction with `INVALID_CONFIG`.

## Schema [#schema]

Platform types and actions live in configuration, not storage. IAM stores the managed side of the catalog:

| Collection      | Key fields                                                                                                                                                                                              |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resources`     | `type`, `resourceId`, `attributes`, `ownerId`, `parentType`, `parentId`, `createdAt`, `updatedAt`. `uniqueKey` is `{type}/{resourceId}` within the tenant.                                              |
| `relationships` | `type`, `resourceId`, `relation`, `subjectType` (`identity` or `group`), `subjectId`, `createdBy`, `createdAt`, `expiresAt`. `uniqueKey` is `{type}/{resourceId}#{relation}@{subjectType}:{subjectId}`. |
| `resourceTypes` | Tenant-defined types: `name` (also the `uniqueKey`), `description`, `attributes`, `parent`, `relations`, `createdAt`.                                                                                   |
| `actions`       | Tenant-defined actions: `name` (also the `uniqueKey`), `description`, `resourceType`.                                                                                                                   |

## Errors [#errors]

| Code                                                                              | When                                                                                                                                                   |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`INVALID_ACTION`](/docs/reference/errors#invalid_action)                         | A policy names an exact action that is not in the catalog, or a tenant action collides with a platform namespace.                                      |
| [`INVALID_RESOURCE_TYPE`](/docs/reference/errors#invalid_resource_type)           | A resource pattern or registration names an unknown type, or a tenant type collides with a reserved or platform name.                                  |
| [`RESOURCE_MISMATCH`](/docs/reference/errors#resource_mismatch)                   | `resolveResource` returned a different type, ID, or tenant than the check asked about.                                                                 |
| [`RESOURCE_RESOLVER_REQUIRED`](/docs/reference/errors#resource_resolver_required) | A check on an application-owned type without a `resolveResource` option.                                                                               |
| [`CATALOG_LOCKED`](/docs/reference/errors#catalog_locked)                         | Tenant catalog calls in the default `'catalog'` mode.                                                                                                  |
| [`RESOURCE_IN_USE`](/docs/reference/errors#resource_in_use)                       | Deleting a type or action that resources, relationships, child types, or policies still use.                                                           |
| [`CONFLICT`](/docs/reference/errors#conflict)                                     | Registering a resource, resource type, or action that already exists. Creating an existing relationship tuple is not an error: it refreshes the tuple. |
| [`NOT_FOUND`](/docs/reference/errors#not_found)                                   | A relationship names a managed resource that is not registered, or a type or action to delete does not exist.                                          |
| [`LIMIT_EXCEEDED`](/docs/reference/errors#limit_exceeded)                         | The tenant's plan limit on registered resources was reached.                                                                                           |
| [`INVALID_CONFIG`](/docs/reference/errors#invalid_config)                         | A configured type or action is invalid, reserved, or declared twice.                                                                                   |

## Next steps [#next-steps]

  - [Catalog](/docs/authorization/catalog): Configuring and validating the catalog in depth.

  - [Relationships](/docs/authorization/relationships): Sharing and ownership with relationship tuples.

  - [Adapters and plugins](/docs/concepts/extensions): Adding actions and resource types from a plugin.
