---
title: "Workspaces"
description: "Workspaces, members, invitations and custom roles."
url: "https://saved.sh/docs/cli/workspaces"
---

A workspace is the tenant. Members, permissions, quotas and billing all belong to it, and
nothing crosses between workspaces.

## Workspaces [#workspaces]

```bash
sctl workspace list
sctl workspace create "analytics"
sctl workspace switch <workspace-id>
sctl workspace switch analytics          # or by name
sctl workspace current
```

`switch` takes an **ID or a name**, matched against the workspaces you belong to, and
`current` reports the one your session is scoped to. You are in exactly one at a time.

Switching re-scopes your credential: the new token is issued for the workspace you are
joining, which is why `switch` resolves the target from your memberships rather than by
reading it. The token you hold cannot read a workspace it is not scoped to.

Creating a workspace makes you its `admin`. It is human-only: a machine credential is refused,
because a workspace that does not exist yet has no permission to check against.

## Members [#members]

```bash
sctl member list
```

```
EMAIL                  MEMBERSHIP           ROLE     STATUS
you@example.com        mem_01H...           admin    active
sam@example.com        mem_01J...           member   active
```

### Inviting [#inviting]

```bash
sctl member invite sam@example.com
sctl member invite sam@example.com admin
```

The role argument is optional. &#x2A;*Omitting it means `member`, which carries no permissions at
all.**

<Callout type="warn">
  A bare `member` sees an empty workspace and cannot do anything. That is deliberate, so nobody
  is granted access by simply existing, but it does mean an invitation without a role usually
  needs a follow-up. Assign a role at invite time unless you mean it.
</Callout>

### Changing a role [#changing-a-role]

```bash
sctl member role <membership-id> admin
sctl member role <membership-id> <custom-role-slug>
```

The first argument is the **membership ID** from `sctl member list`, not the email or the user
ID. A membership is the join between a person and a workspace, which is the thing a role
attaches to.

### Removing [#removing]

```bash
sctl member remove <membership-id>
```

<Callout type="error">
  **Removing a member does not revoke API keys or workers they created.** Those belong to the
  workspace, not to the person, so automation keeps working when someone leaves. Offboarding
  therefore has a second step: review `sctl apikey list` and `sctl worker list` and revoke
  anything you cannot account for.
</Callout>

### Invitations [#invitations]

```bash
sctl member invitations
sctl member resend <invitation-id>
sctl member revoke <invitation-id>
```

Invitations expire. `resend` issues a fresh one; `revoke` cancels it before it is accepted.

## Roles [#roles]

Roles bundle permissions onto a membership. The backend authorizes on the resolved permissions
and never reads the role itself, which is why a custom role you invent is enforced correctly
with no change from us.

```bash
sctl role list
```

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

### Creating a custom role [#creating-a-custom-role]

```bash
sctl role create "Auditor" \
  --desc "Read-only, including the audit trail" \
  --perms workspaces:read,backups:read,artifacts:read,audit:read
```

`--perms` is a comma-separated list of permission slugs. The full catalog:

```bash
sctl auth whoami      # what you personally hold
```

See [permissions](/docs/security/permissions#the-catalog) for all 27.

<Callout>
  **The distinction worth getting right:** `artifacts:read` lets someone see that an artifact
  exists, its size and its checksum. `artifacts:download` lets them read the contents. Most
  people need the first and not the second, and separating them is the point of the split.
</Callout>

### Updating and deleting [#updating-and-deleting]

```bash
sctl role update <slug> --desc "..." --perms backups:read,artifacts:read
sctl role delete <slug>
```

`update` replaces the permission set rather than adding to it. Send the full list you want.

Roles are referenced by **slug**, which `sctl role list` prints. Deleting a role that
memberships still hold leaves those people without its permissions, so change their role
first.

## Useful shapes [#useful-shapes]

| 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   |

## Quotas [#quotas]

| Limit                  | Trial | Paid |
| ---------------------- | ----- | ---- |
| Members per workspace  | 1     | 25   |
| Workspaces per user    | 5     | 5    |
| Workers per workspace  | 1     | 10   |
| Backups per workspace  | 5     | 100  |
| API keys per workspace | 2     | 20   |

Exceeding a limit fails with a quota error naming the resource, rather than succeeding partly.

## Next [#next]

<Cards>
  <Card href="/docs/security/permissions" title="Permissions" description="The catalog roles draw from." />

  <Card href="/docs/cli/api-keys" title="API keys" description="Credentials for machines rather than people." />
</Cards>
