---
title: User Group
kind: resource
slug: user-group
summary: "A named collection of people you can grant Droplet access to in one shot. The roster is the membership; visibility controls who sees the row."
---

# User Group

A named collection of people you can grant Droplet access to in one shot. The roster is the membership; visibility controls who sees the row.

A **User Group** is a named bag of people. Grant a [Droplet](/docs/resources/droplet) to `@engineering` and you grant it to all 24 current members; add a 25th member later and they gain access without anyone re-saving. Distinct from [Spaces](/docs/resources/space) — Spaces group *content*; Groups group *people*. Two **system role groups** (`@drop-people-admins`, `@drop-library-admins`) ship today; they carry `systemRole: true`, live pinned at the top of the Groups directory, and replace the older per-user admin flags.

## The User Group object

### Attributes

- **`slug`** `string` — URL-friendly identifier. Used in `/directory/groups/{slug}` and as the `@{slug}` token when sharing Droplets. Globally unique, max 40 chars, lowercase alphanumeric + hyphens. Renames record a 30-day soft redirect so existing links keep resolving.
- **`name`** `string` — Display name. Max 80 chars.
- **`description`** `string · nullable` — Markdown description. Rendered server-side with a constrained sanitizer (strips images and external URLs in `href`).
- **`visibility`** `enum` (enum: `PUBLIC` / `MEMBER_ONLY` / `ADMIN_ONLY`) — **Who can see this Group.** `PUBLIC` — listed in the directory, roster visible to everyone. `MEMBER_ONLY` — listed in the directory, roster hidden from non-members (count still shown). `ADMIN_ONLY` — invisible to non-admins; only People Admins can navigate to it. System role groups are pinned to `ADMIN_ONLY`.
- **`systemRole`** `boolean` — When `true`, this Group is a curated system role (`@drop-people-admins` or `@drop-library-admins`). System role groups cannot be deleted, cannot have their slug renamed, cannot leave `ADMIN_ONLY`, and the last-remaining member can only be removed by a site admin (`User.isAdmin`).
- **`source`** `enum` (enum: `MANUAL` / `SLACK_SYNCED`) — `MANUAL` — created in Drop by a People Admin. `SLACK_SYNCED` — reserved for V2 (mirrored from a Slack usergroup); no synced groups exist today.
- **`tags`** `string[]` — Lowercase tag strings, ≤24 chars each, ≤10 per group. Used by the directory tag filter. UI for editing tags was removed in the 032 follow-up; the field is preserved for future taxonomy work.
- **`linkedSpaceId`** `string · nullable` — Optional pointer to a paired [Space](/docs/resources/space) (e.g. `@engineering` Group links to the `engineering` Space). Pure cross-reference — no membership sync.
- **`slackHandle`** `string · nullable` — Reserved for V2 Slack-sync. Unique-when-set. Today: always `null` for `MANUAL` groups.
- **`id`** `string · less-common` — CUID primary key. Use `slug` in API paths.
- **`slackId`** `string · nullable · less-common` — V2-reserved Slack usergroup id (`S0123456`). Unique-when-set. Today: always `null`.
- **`createdById`** `string · less-common` — The User who created the Group. Carried for audit only — does not confer special permissions (People Admin role does).
- **`createdAt`** `timestamp · less-common` — ISO 8601 timestamp of creation.
- **`updatedAt`** `timestamp · less-common` — ISO 8601 timestamp of last metadata update.

### JSON example

```json
{
  "id": "clxgrp2...",
  "slug": "engineering",
  "name": "Engineering",
  "description": "The team building Drop and adjacent tools.",
  "tags": ["team"],
  "source": "MANUAL",
  "visibility": "PUBLIC",
  "systemRole": false,
  "linkedSpaceId": null,
  "slackHandle": null,
  "slackId": null,
  "createdById": "usr_alex",
  "createdAt": "2026-05-20T10:00:00.000Z",
  "updatedAt": "2026-05-28T09:00:00.000Z"
}
```

## Methods

### List User Groups

`GET /api/user-groups`

Return User Groups visible to the caller. Non-admins never see `ADMIN_ONLY` rows (the visibility filter strips them server-side). Each row carries `systemRole` so clients can render the pinned "System Roles" section. Supports `?q=` (substring across name/slug/description + exact tag) and `?tag=` (exact tag, case-folded).

**Parameters**

- **`q`** _(string · query)_ — Substring search across name, slug, description; also matches an exact tag.
- **`tag`** _(string · query)_ — Exact tag filter (lowercased server-side).

**CLI**

```bash
drop groups list --q engineering
```

**MCP**

```json
{
  "tool": "drop_groups",
  "arguments": {
    "q": "engineering"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/user-groups' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
[
  {
    "id": "clxgrp2...",
    "slug": "engineering",
    "name": "Engineering",
    "descriptionPreview": "The team building Drop and adjacent tools.",
    "memberCount": 24,
    "tags": [
      "team"
    ],
    "source": "MANUAL",
    "visibility": "PUBLIC",
    "systemRole": false
  }
]
```

### Create a User Group

`POST /api/user-groups`

**People Admin only.** Create a new Group. Slug must be unique across all Groups. If the slug collides with an existing [Space](/docs/resources/space) slug, the response includes a non-blocking `collisionWarning` so the UI can offer to link the two as a cross-reference.

**Parameters**

- **`name`** _(string · body · required)_ — Group display name (1–80 chars).
- **`slug`** _(string · body · required)_ — URL slug — lowercase alphanumerics + hyphens, 3–40 chars.
- **`description`** _(string · body)_ — Markdown description (max 10k chars).
- **`tags`** _(string[] · body)_ — Up to 10 lowercase tag strings.
- **`visibility`** _(enum · body)_ — PUBLIC | MEMBER_ONLY | ADMIN_ONLY. Default: PUBLIC.

**CLI**

```bash
drop groups create --name Engineering --slug engineering --description "The team building Drop and adjacent tools." --visibility PUBLIC
```

**MCP**

```json
{
  "tool": "drop_create_group",
  "arguments": {
    "name": "Engineering",
    "slug": "engineering",
    "description": "The team building Drop and adjacent tools.",
    "visibility": "PUBLIC"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/user-groups' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Engineering","slug":"engineering","description":"The team building Drop and adjacent tools.","visibility":"PUBLIC"}'
```

**Response** (201)

```json
{
  "group": {
    "id": "clxgrp2...",
    "slug": "engineering",
    "name": "Engineering",
    "descriptionPreview": "The team building Drop and adjacent tools.",
    "memberCount": 0,
    "tags": [],
    "source": "MANUAL",
    "visibility": "PUBLIC",
    "systemRole": false,
    "description": "The team building Drop and adjacent tools.",
    "descriptionHtml": "<p>The team building Drop and adjacent tools.</p>",
    "linkedSpace": null,
    "slackHandle": null,
    "slackId": null,
    "createdAt": "2026-06-03T10:00:00.000Z",
    "updatedAt": "2026-06-03T10:00:00.000Z",
    "createdBy": {
      "id": "usr_alex",
      "email": "alex@himarley.com",
      "name": "Alex Chen"
    }
  },
  "collisionWarning": null
}
```

### Retrieve a User Group

`GET /api/user-groups/{slug}`

Return full Group detail — metadata, member roster, Droplet grants, linked Space, and a `systemRole` flag. `ADMIN_ONLY` groups return 404 to non-admins; `MEMBER_ONLY` groups return an empty roster to non-members. Renamed slugs return `301` with `Location` pointing at the current slug (30-day window).

**Parameters**

- **`slug`** _(string · path · required)_ — The Group slug.

**CLI**

```bash
drop groups get engineering
```

**MCP**

```json
{
  "tool": "drop_get_group",
  "arguments": {
    "slug": "engineering"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/user-groups/engineering' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "group": {
    "id": "clxgrp2...",
    "slug": "engineering",
    "name": "Engineering",
    "descriptionPreview": "The team building Drop and adjacent tools.",
    "memberCount": 24,
    "tags": [
      "team"
    ],
    "source": "MANUAL",
    "visibility": "PUBLIC",
    "systemRole": false,
    "description": "The team building Drop and adjacent tools.",
    "descriptionHtml": "<p>The team building Drop and adjacent tools.</p>",
    "linkedSpace": null,
    "slackHandle": null,
    "slackId": null,
    "createdAt": "2026-05-20T10:00:00.000Z",
    "updatedAt": "2026-05-28T09:00:00.000Z",
    "createdBy": {
      "id": "usr_alex",
      "email": "alex@himarley.com",
      "name": "Alex Chen"
    }
  },
  "members": [
    {
      "userId": "usr_alex",
      "email": "alex@himarley.com",
      "name": "Alex Chen",
      "title": "Engineering",
      "addedAt": "2026-05-20T10:00:00.000Z",
      "addedBy": "usr_alex"
    }
  ],
  "activity": [],
  "dropletGrants": [
    {
      "slug": "team-roadmap",
      "name": "Team Roadmap",
      "sharedAt": "2026-05-22T12:00:00.000Z"
    }
  ],
  "isPeopleAdmin": false
}
```

### Update a User Group

`PATCH /api/user-groups/{slug}`

**People Admin only.** Patch `name`, `slug` (via `newSlug`), `description`, `tags`, `visibility`, or `linkedSpaceId`. Slug renames park the old slug as a 30-day soft redirect. System role groups (`systemRole: true`) reject `visibility` changes off `ADMIN_ONLY` (`400 SYSTEM_ROLE_ADMIN_ONLY`) and reject slug renames outright (`400 SYSTEM_ROLE_SLUG_LOCKED`); name / description / tags remain editable.

**Parameters**

- **`slug`** _(string · path · required)_ — Current Group slug.
- **`name`** _(string · body)_ — New display name.
- **`newSlug`** _(string · body)_ — New slug — records a 30-day redirect on the old one.
- **`description`** _(string | null · body)_ — Markdown description; null to clear.
- **`tags`** _(string[] · body)_ — Replace tag list (max 10).
- **`linkedSpaceId`** _(string | null · body)_ — Cross-reference Space id, or null to clear.
- **`visibility`** _(enum · body)_ — PUBLIC | MEMBER_ONLY | ADMIN_ONLY.

**CLI**

```bash
drop groups update engineering --description "The team building Drop." --visibility MEMBER_ONLY
```

**MCP**

```json
{
  "tool": "drop_update_group",
  "arguments": {
    "slug": "engineering",
    "description": "The team building Drop.",
    "visibility": "MEMBER_ONLY"
  }
}
```

**HTTP**

```bash
curl -X PATCH 'https://drop.himarley.ai/api/user-groups/engineering' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"description":"The team building Drop.","visibility":"MEMBER_ONLY"}'
```

**Response** (200)

```json
{
  "id": "clxgrp2...",
  "slug": "engineering",
  "name": "Engineering",
  "descriptionPreview": "The team building Drop.",
  "memberCount": 24,
  "tags": [
    "team"
  ],
  "source": "MANUAL",
  "visibility": "MEMBER_ONLY",
  "systemRole": false,
  "description": "The team building Drop.",
  "descriptionHtml": "<p>The team building Drop.</p>",
  "linkedSpace": null,
  "slackHandle": null,
  "slackId": null,
  "createdAt": "2026-05-20T10:00:00.000Z",
  "updatedAt": "2026-06-03T11:00:00.000Z",
  "createdBy": {
    "id": "usr_alex",
    "email": "alex@himarley.com",
    "name": "Alex Chen"
  }
}
```

### Delete a User Group

`DELETE /api/user-groups/{slug}`

**People Admin only.** Before the Group is dropped, every `AppAccess` row that grants via this Group is rewritten to individual email grants for the Group's current members — access is never silently revoked. Returns a summary of the expansion. System role groups reject with `403 SYSTEM_ROLE_PROTECTED`.

**Parameters**

- **`slug`** _(string · path · required)_ — The Group slug.

**CLI**

```bash
drop groups delete engineering
```

**MCP**

```json
{
  "tool": "drop_delete_group",
  "arguments": {
    "slug": "engineering"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/user-groups/engineering' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "grantsExpanded": 2,
  "dropletSlugs": [
    "team-roadmap",
    "engineering-handbook"
  ]
}
```

### List members

`GET /api/user-groups/{slug}/members`

Return every member of the Group. For `MEMBER_ONLY` groups, non-members get an empty roster; admins always see everything.

**Parameters**

- **`slug`** _(string · path · required)_ — The Group slug.

**CLI**

```bash
drop groups members list engineering
```

**MCP**

```json
{
  "tool": "drop_group_members",
  "arguments": {
    "slug": "engineering"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/user-groups/engineering/members' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
[
  {
    "userId": "usr_alex",
    "email": "alex@himarley.com",
    "name": "Alex Chen",
    "title": "Engineering",
    "addedAt": "2026-05-20T10:00:00.000Z",
    "addedBy": "usr_alex"
  }
]
```

### Add member(s)

`POST /api/user-groups/{slug}/members`

**People Admin only** (or member of a system role group when curating it). Bulk-add members. Idempotent — already-members and unknown ids are silently skipped. New members immediately gain access to every Droplet shared with this Group (CloudFront allowlists re-synced fire-and-forget).

**Parameters**

- **`slug`** _(string · path · required)_ — The Group slug.
- **`userIds`** _(string[] · body · required)_ — 1–50 user ids to add.

**CLI**

```bash
drop groups members add engineering --user-ids usr_kai,usr_jane
```

**MCP**

```json
{
  "tool": "drop_add_group_members",
  "arguments": {
    "slug": "engineering",
    "userIds": [
      "usr_kai",
      "usr_jane"
    ]
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/user-groups/engineering/members' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"userIds":["usr_kai","usr_jane"]}'
```

**Response** (200)

```json
{
  "added": 2,
  "members": [
    {
      "userId": "usr_alex",
      "email": "alex@himarley.com",
      "name": "Alex Chen",
      "title": "Engineering",
      "addedAt": "2026-05-20T10:00:00.000Z",
      "addedBy": "usr_alex"
    },
    {
      "userId": "usr_kai",
      "email": "kai@himarley.com",
      "name": "Kai Mitchell",
      "title": null,
      "addedAt": "2026-05-31T09:15:00.000Z",
      "addedBy": "usr_alex"
    },
    {
      "userId": "usr_jane",
      "email": "jane@himarley.com",
      "name": "Jane Doe",
      "title": null,
      "addedAt": "2026-05-31T09:15:00.000Z",
      "addedBy": "usr_alex"
    }
  ]
}
```

### Remove member

`DELETE /api/user-groups/{slug}/members/{userId}`

**People Admin only.** Remove a single member. For system role groups, a last-admin guard runs inside the same transaction as the delete + audit: removing the only remaining member rejects with `400 LAST_SYSTEM_ROLE_MEMBER`. Site admins (`User.isAdmin`) bypass the guard.

**Parameters**

- **`slug`** _(string · path · required)_ — The Group slug.
- **`userId`** _(string · path · required)_ — The user id to remove.

**CLI**

```bash
drop groups members remove engineering usr_kai
```

**MCP**

```json
{
  "tool": "drop_remove_group_member",
  "arguments": {
    "slug": "engineering",
    "userId": "usr_kai"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/user-groups/engineering/members/usr_kai' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "ok": true
}
```

### Bootstrap a Space

`POST /api/user-groups/{slug}/bootstrap-space`

Convenience: create a new [Space](/docs/resources/space) seeded from this Group's current roster, then set `linkedGroupId` / `linkedSpaceId` on both sides so the cross-reference is in place. **People Admin only.** No ongoing membership sync — the seed is one-shot.

**Parameters**

- **`slug`** _(string · path · required)_ — The Group slug.

**CLI**

```bash
drop groups bootstrap-space engineering
```

**MCP**

```json
{
  "tool": "drop_bootstrap_space_from_group",
  "arguments": {
    "slug": "engineering"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/user-groups/engineering/bootstrap-space' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (201)

```json
{
  "spaceSlug": "engineering",
  "memberCount": 24
}
```
