# Members (/docs/auth/members)



Members are the operators and services that act inside a workspace. This page is
about the member **lifecycle** — inviting, role assignment, suspension, and
removal. The RBAC roles-and-scopes *model* itself lives in
[Authorization](/docs/auth/authorization); this page does not re-explain it.

All endpoints are under `/api/v2/members` and are served by `MemberService`.

## What a member actually is [#what-a-member-actually-is]

Ductor has &#x2A;*no first-class user record.** A workspace member is modeled
honestly as a **tenant API key** (a `tenant_api_key` row) plus the **RBAC role
bundle** that key carries. The member's ID *is* the underlying API key ID.

<Callout type="info">
  Because a member is a key, the member roster and your [API keys](/docs/management/api-keys)
  are two views of the same underlying identities. Managing a member's role here
  changes what that key is authorized to do.
</Callout>

Each member is classified by `kind`:

* **`MEMBER_KIND_HUMAN`** — an operator-managed key.
* **`MEMBER_KIND_SERVICE`** — an inter-process identity.

A `MemberRecord` exposes the identity without ever revealing the raw secret:

<TypeTable
  type="{
  id: { type: &#x22;string&#x22;, description: &#x22;Member identifier — the underlying API key ID.&#x22; },
  kind: { type: &#x22;enum&#x22;, description: &#x22;MEMBER_KIND_HUMAN or MEMBER_KIND_SERVICE.&#x22; },
  display_name: { type: &#x22;string&#x22;, description: &#x22;Human-readable label for the member.&#x22; },
  key_prefix: { type: &#x22;string&#x22;, description: 'First characters of the raw key for identification (e.g. &#x22;duk_abc1&#x22;). Never the full key.' },
  role: { type: &#x22;string&#x22;, description: &#x22;Assigned RBAC role bundle name. Empty when the member has no bundle.&#x22; },
  scopes: { type: &#x22;string[]&#x22;, description: &#x22;Resolved effective scope set (role expansion ∪ per-key scopes).&#x22; },
  status: { type: &#x22;string&#x22;, description: '&#x22;active&#x22; or &#x22;revoked&#x22;.' },
  created_at: { type: &#x22;date-time&#x22;, description: &#x22;When the member (key) was created.&#x22; },
  last_active_at: { type: &#x22;date-time&#x22;, description: &#x22;When the member last authenticated. Unset if never.&#x22; },
}"
/>

## The roster [#the-roster]

`GET /api/v2/members` returns the roster for the authenticated tenant;
`GET /api/v2/members/{id}` returns one member by its key ID. Both require the
`tenant:read` scope.

```bash
curl http://localhost:8080/api/v2/members \
  -H "X-Tenant-ID: $DUCTOR_TENANT_ID"
```

## Lifecycle [#lifecycle]

```mermaid
flowchart LR
  I[Invite] --> P[Pending invite]
  P -->|accepted| A[Active member]
  P -->|revoke| X[Cancelled]
  A -->|disable| D[Disabled]
  D -->|reenable| A
  A -->|remove| R[Revoked]
```

### Invites [#invites]

Invites create a pending member before a key is active.

<TypeTable
  type="{
  &#x22;POST /invites&#x22;: { description: &#x22;Create a pending invite. Requires email; optional roles[] and message.&#x22; },
  &#x22;POST /invites/{invite_id}/resend&#x22;: { description: &#x22;Re-issue the invite email for a pending invite.&#x22; },
  &#x22;POST /invites/{invite_id}/revoke&#x22;: { description: &#x22;Cancel a pending invite.&#x22; },
}"
/>

```bash
curl -X POST http://localhost:8080/api/v2/members/invites \
  -H "X-Tenant-ID: $DUCTOR_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{ "email": "ops@example.com", "roles": ["operator"] }'
```

### Roles and status [#roles-and-status]

<TypeTable
  type="{
  &#x22;POST /{id}/role&#x22;: { description: &#x22;Set the RBAC role bundle. Passing an empty list clears it; unknown role names are rejected.&#x22; },
  &#x22;POST /{id}/disable&#x22;: { description: &#x22;Suspend a member without deleting data.&#x22; },
  &#x22;POST /{id}/reenable&#x22;: { description: &#x22;Restore a previously disabled member.&#x22; },
  &#x22;POST /{id}/remove&#x22;: { description: &#x22;Revoke the underlying key and audit the removal.&#x22; },
}"
/>

`AssignRole` sets the bundle for a member. The role names must come from
`ListRoles` (below) — the change is rejected if any name is unknown. Passing an
**empty** role list clears the bundle, after which the member falls back to its
per-key scope list, if any.

```bash
curl -X POST http://localhost:8080/api/v2/members/$MEMBER_ID/role \
  -H "X-Tenant-ID: $DUCTOR_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{ "roles": ["admin"] }'
```

<Callout type="warn">
  `remove` revokes the underlying API key. Any client still presenting that key
  stops authenticating immediately. Use `disable` for a reversible suspension.
</Callout>

## Role and scope catalogs [#role-and-scope-catalogs]

Two read-only catalogs back role assignment. They are pass-through accessors over
the static RBAC catalogs described in [Authorization](/docs/auth/authorization) —
consult that page for what the roles and scopes *mean*.

* **`GET /api/v2/members/roles`** — every role name and the scopes it expands to,
  including the built-in roles (`viewer`, `operator`, `admin`, `service`) plus
  any roles registered by extension modules.
* **`GET /api/v2/members/scopes`** — the full catalog of legal `resource:action`
  scope identifiers known to the authorization layer.

## Related [#related]

<Cards>
  <Card title="Authorization" href="/docs/auth/authorization" description="The RBAC roles-and-scopes model, deny-by-default evaluation, and the scope catalog." />

  <Card title="API Keys" href="/docs/management/api-keys" description="Managing the tenant API keys that members are built on." />
</Cards>
