---
title: "Roles"
description: "Bundling permissions onto a membership, addressed by slug."
url: "https://saved.sh/docs/api/roles"
---

A role bundles permissions. The backend authorizes on the resolved permissions and never reads
the role itself, which is why a custom role you invent is enforced correctly without us
knowing it exists.

## List [#list]

```bash
curl -fsS "https://api.saved.sh/v1/workspaces/$WID/roles" \
  -H "Authorization: Bearer $TOKEN"
```

```json
{
  "data": [
    { "slug": "admin",  "name": "Admin",  "description": "Full access", "permissions": ["..."] },
    { "slug": "member", "name": "Member", "description": "",            "permissions": [] },
    { "slug": "org_auditor", "name": "Auditor", "description": "Read-only",
      "permissions": ["workspaces:read", "backups:read", "artifacts:read", "audit:read"] }
  ],
  "next_cursor": null
}
```

Permission: `roles:read`.

| Role     | Carries                         |
| -------- | ------------------------------- |
| `admin`  | Every permission in the catalog |
| `member` | **Nothing**, by default         |
| Custom   | Whatever you grant              |

<Callout type="warn">
  A bare `member` has no access at all. That is deliberate, so nobody is granted anything by
  simply existing, but it means an invitation without a role always needs a follow-up.
</Callout>

Custom roles are auto-prefixed (`org_`) by the identity provider. Use the `slug` exactly as
returned.

## Create [#create]

```bash
curl -fsS -X POST "https://api.saved.sh/v1/workspaces/$WID/roles" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "Auditor",
        "description": "Read-only, including the audit trail",
        "permissions": ["workspaces:read", "backups:read", "artifacts:read", "audit:read"]
      }'
```

Permission: `roles:write`. Slugs are derived from the name.

Validate against the live catalog rather than a hard-coded list:

```bash
curl -fsS https://api.saved.sh/v1/permissions -H "Authorization: Bearer $TOKEN"
```

```json
{ "data": ["workspaces:read", "workspaces:write", "members:read", "..."] }
```

## Update [#update]

```bash
curl -fsS -X PATCH "https://api.saved.sh/v1/workspaces/$WID/roles/$SLUG" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"permissions": ["backups:read", "artifacts:read"]}'
```

<Callout type="warn">
  `permissions` **replaces** the set rather than adding to it. Send the full list you want, or
  you will silently remove the ones you left out.
</Callout>

Permission changes take effect when the affected members' tokens are next minted, which is on
their next sign-in or token refresh rather than instantly.

## Delete [#delete]

```bash
curl -fsS -X DELETE "https://api.saved.sh/v1/workspaces/$WID/roles/$SLUG" \
  -H "Authorization: Bearer $TOKEN"
```

Permission: `roles:write`. Returns `204`.

Deleting a role that memberships still hold leaves those people without its permissions. Move
them to another role first.

`admin` and `member` are seeded and cannot be deleted.

## Designing roles [#designing-roles]

The distinction that matters most is between knowing about a backup and reading its contents.

| Role     | Permissions                                                       | For                                      |
| -------- | ----------------------------------------------------------------- | ---------------------------------------- |
| Auditor  | `workspaces:read`, `backups:read`, `artifacts:read`, `audit:read` | Compliance review with no access to data |
| Operator | The above plus `backups:write`, `workers:read`                    | Runs the backups, cannot exfiltrate      |
| Restorer | `backups:read`, `artifacts:read`, `artifacts:download`            | Can actually recover, and nothing else   |

`artifacts:read` returns sizes, checksums and key IDs. `artifacts:download` returns the data.
Most people need only the first.

## Multiple roles [#multiple-roles]

A membership may hold several roles, and the effective permission set is their **union**. The
backend sees one flat set and does not know how it was assembled.

## Next [#next]

<Cards>
  <Card href="/docs/security/permissions" title="Permissions" description="The full catalog, and machine limits." />

  <Card href="/docs/api/members" title="Members" description="Assigning a role to a membership." />
</Cards>
