> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fga.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Roles

> How role assignment maps to FGA tuples for every place a role can attach in Auth0 — a user within an organization, a group at the tenant level, a group within an organization — plus how to model static vs. dynamic roles.

This page is a reference for role assignment, since roles can attach to three different entities, and any of the other use-cases can also involve roles.

A role can attach to a user within an organization, a group at the tenant level, or a group within an organization. Each attachment point fires its own event pair and writes to a different object. Pick the subsection below that matches how you're assigning the role, then read [Static vs. dynamic roles](#static-vs-dynamic-roles), which applies regardless of which one you picked.

## What can hold a role

### A user, within an organization membership

Triggered by `organization.member.role.assigned` / `organization.member.role.deleted`. This is the most common case, and the one [B2B via Organizations](https://docs.fga.dev/relationship-sync/use-cases/b2b-organizations.md) walks through end to end.

`organization.member.deleted`'s payload only ever carries `organization.id` and `user.user_id`, never which roles that user held. So a role assignment is wired through an intermediate `organization_membership` object instead of directly from `user` to `role`: that's what lets removing a user from an organization revoke every role they held in it with a pair of filters keyed on the membership, without knowing what those roles were.

FGA model · DSL

```yaml theme={null}
rules:
  - name: "Add user to organization"
    when: input.type == "organization.member.added"
    variables:
      organization_id: input.data.object.organization.id
      user_id: input.data.object.user.user_id
    tuples:
      - user: "user:{{ variables.user_id }}"
        relation: "member"
        object: "organization:{{ variables.organization_id }}"

  - name: "Remove user from organization"
    when: input.type == "organization.member.deleted"
    variables:
      organization_id: input.data.object.organization.id
      user_id: input.data.object.user.user_id
    tuples:
      - action: delete
        user: "user:{{ variables.user_id }}"
        relation: "member"
        object: "organization:{{ variables.organization_id }}"

  - name: "Revoke roles held through the membership"
    when: input.type == "organization.member.deleted"
    variables:
      organization_id: input.data.object.organization.id
      user_id: input.data.object.user.user_id
    tuple_filters:
      - user: "organization_membership:{{ variables.organization_id }}_{{ variables.user_id }}#member"
        object: "role:"
        action: delete
      - object: "organization_membership:{{ variables.organization_id }}_{{ variables.user_id }}"
        action: delete

  - name: "Assign role to user in organization"
    when: input.type == "organization.member.role.assigned"
    variables:
      organization_id: input.data.object.organization.id
      user_id: input.data.object.user.user_id
      role_id: input.data.object.role.id
    tuples:
      - user: "user:{{ variables.user_id }}"
        relation: "member"
        object: "organization_membership:{{ variables.organization_id }}_{{ variables.user_id }}"
      - user: "organization_membership:{{ variables.organization_id }}_{{ variables.user_id }}#member"
        relation: "assignee"
        object: "role:{{ variables.role_id }}"

  - name: "Remove role from user in organization"
    when: input.type == "organization.member.role.deleted"
    variables:
      organization_id: input.data.object.organization.id
      user_id: input.data.object.user.user_id
      role_id: input.data.object.role.id
    tuples:
      - action: delete
        user: "organization_membership:{{ variables.organization_id }}_{{ variables.user_id }}#member"
        relation: "assignee"
        object: "role:{{ variables.role_id }}"
```

Assigning a role writes both `user --member--> organization_membership:{organization_id}_{user_id}` and `organization_membership:{organization_id}_{user_id}#member --assignee--> role:{role_id}`. "Remove user from organization" removes the user's `organization` membership tuple, and "Revoke roles held through the membership" cleans up the rest with two filters, both computable from `organization_id` and `user_id`, which `organization.member.deleted` carries: one deletes every `role` assignment granted to that membership's `#member` userset, the other deletes the membership's own `member` tuples. Without the first filter, the user would lose access (their `member` tuple is gone) but the `assignee` tuples on each role would remain as orphans. A simpler model, `role` with `define assignee: [user]` and the role's assignees granted directly on the resource (for example `define billing_manager: [role#assignee]` on `organization`), works when you don't need roles revoked automatically on membership removal: a delete would have to name each role, and the event doesn't say which ones the user held. If the same user holds the same role via a second organization they're still a member of, each organization gets its own `organization_membership` object, so removing one doesn't affect the other.

### A group, at the tenant level

Triggered by `group.role.assigned` / `group.role.deleted`. Covers assigning a role directly to a SCIM group with no organization involved, as in [Workforce via SCIM Groups](https://docs.fga.dev/relationship-sync/use-cases/workforce-scim-groups.md).

FGA model · DSL

```yaml theme={null}
rules:
  - name: "Assign role to group"
    when: input.type == "group.role.assigned"
    variables:
      group_id: input.data.object.group.id
      role_id: input.data.object.role.id
    tuples:
      - user: "group:{{ variables.group_id }}#member"
        relation: "assignee"
        object: "role:{{ variables.role_id }}"

  - name: "Remove role from group"
    when: input.type == "group.role.deleted"
    variables:
      group_id: input.data.object.group.id
      role_id: input.data.object.role.id
    tuples:
      - action: delete
        user: "group:{{ variables.group_id }}#member"
        relation: "assignee"
        object: "role:{{ variables.role_id }}"
```

`role#assignee` accepting `group#member` (alongside `user`) is what lets a role reach every current and future member of a group with one tuple, instead of one tuple per member.

`group.role.deleted` only carries the role's `id`, never its `name`. If a rule needs to branch on which role fired, branch on `role.id`.

### A group, within an organization

Triggered by `organization.group.role.assigned` / `organization.group.role.deleted`, distinct from the bare, tenant-level events above. The payload carries `organization.id`, `group.id`, and `role.id` together.

Whether this should write to the same `role:{id}#assignee` tuple as the tenant-level case, or a separate one, depends on whether a group in your tenant can hold the same role at both scopes and you need to tell the two grants apart. If a group only ever gets a given role one way, write to the same tuple as the tenant-level case:

```yaml theme={null}
rules:
  - name: "Assign role to group in organization"
    when: input.type == "organization.group.role.assigned"
    variables:
      group_id: input.data.object.group.id
      role_id: input.data.object.role.id
    tuples:
      - user: "group:{{ variables.group_id }}#member"
        relation: "assignee"
        object: "role:{{ variables.role_id }}"

  - name: "Remove role from group in organization"
    when: input.type == "organization.group.role.deleted"
    variables:
      group_id: input.data.object.group.id
      role_id: input.data.object.role.id
    tuples:
      - action: delete
        user: "group:{{ variables.group_id }}#member"
        relation: "assignee"
        object: "role:{{ variables.role_id }}"
```

If a group can hold the same role both ways and the two need to stay distinguishable, wire this through a separate object instead, the same way user-in-organization role assignment is wired through `organization_membership` above: an object scoped to `(group, organization)`, with its own member tuple, so each grant can be told apart by which object it's attached to and cleaned up independently of the other.

## Static vs. dynamic roles

Roles fall into two categories, and each needs different modeling. This applies to any of the three attachment points above.

* **Static roles**: your application defines these itself, once, as a fixed set of named permissions hardcoded into your model as relations (for example, an "Approver" role your app checks for explicitly). Your mapping can branch on these by identity.
* **Dynamic roles**: each customer or organization defines their own roles, with arbitrary names, IDs, and permissions your application can't know ahead of time. Since they're managed by the customer rather than fixed in your model, these have to be modeled generically and managed as tuples.

Some applications need only one category; others need both.

> [!WARNING]
>
> Branch on the role's **ID** (`input.data.object.role.id`), not its **name**. A role's name can be changed by whoever manages roles in your Auth0 tenant (see [Create Roles](https://auth0.com/docs/manage-users/access-control/configure-core-rbac/roles/create-roles)), but its ID is stable for the role's lifetime. Matching by name is fragile: a rename silently breaks the branch, with no error to catch it.

### Static roles

Since your application already knows about these roles, model each as its own named relation directly on `organization`, for example:

FGA model · DSL

Map it by matching the role's ID, hardcoded to the specific role you created in your own tenant:

```yaml theme={null}
- name: "Assign approver role to user in organization"
  when: >-
    input.type == "organization.member.role.assigned" &&
    input.data.object.role.id == "rol_YOUR_APPROVER_ROLE_ID"
  variables:
    organization_id: input.data.object.organization.id
    user_id: input.data.object.user.user_id
  tuples:
    - user: "user:{{ variables.user_id }}"
      relation: "approver"
      object: "organization:{{ variables.organization_id }}"

- name: "Remove approver role from user in organization"
  when: >-
    input.type == "organization.member.role.deleted" &&
    input.data.object.role.id == "rol_YOUR_APPROVER_ROLE_ID"
  variables:
    organization_id: input.data.object.organization.id
    user_id: input.data.object.user.user_id
  tuples:
    - action: delete
      user: "user:{{ variables.user_id }}"
      relation: "approver"
      object: "organization:{{ variables.organization_id }}"
```

Replace `rol_YOUR_APPROVER_ROLE_ID` with the real role ID from your tenant. Because this writes a direct relation on `organization`, no separate cleanup rule is needed: [Cleaning up roles on membership removal](https://docs.fga.dev/relationship-sync/mapping-language.md#cleaning-up-roles-on-membership-removal)'s filter-by-user+organization technique already reaches it when the membership is removed.

### Dynamic roles

Since these roles' names, IDs, and count aren't known ahead of time, model them generically as `role:{id}#assignee`. Use whichever attachment point above matches how the role is assigned: through `organization_membership` for a user in an organization, or directly on `group#member` for a group.

### Combining both

If your application has both kinds, add two separate rule pairs per event, each guarded by whether the role ID matches your known, hardcoded set. The first pair is the static branch shown above. The second pair reuses the same rules as [A user, within an organization membership](#a-user-within-an-organization-membership), with `input.data.object.role.id != "rol_YOUR_APPROVER_ROLE_ID"` (and any other static role IDs) added to their `when` guards, so the two branches don't overlap.

Rules are independent, and all matching ones fire for a given event, so ordering between the two pairs doesn't matter as long as their `when` conditions are mutually exclusive.
