APIRoles

Roles

A Role is a named bundle of authority within one group. Roles carry a priority (higher = more authority), an optional color, a flag indicating whether they should be auto-assigned to new members, and a list of permission keys (populated via the grant / revoke routes documented below).

The group-scoped routes (POST /v1/groups/:id/roles and GET /v1/groups/:id/roles) live on this page; so do the by-id routes (GET /v1/roles/:id, PATCH /v1/roles/:id, DELETE /v1/roles/:id) and the permission-management routes (POST /v1/roles/:id/permissions, DELETE /v1/roles/:id/permissions/:permission).

Wire format

Role is the shape returned by every endpoint that emits it. Timestamps are ISO 8601 strings.

FieldTypeNotes
idstringServer-generated cuid.
groupIdstringThe group this role belongs to.
namestring1-64 characters. Unique within the group.
prioritynumberInteger. Higher = more authority. Used by the SDK’s “can-act-on” helpers.
colorstring | nullA 7-character hex color (e.g. "#ff5050") if present, null otherwise.
isDefaultbooleanA per-role flag the dev can set; multiple roles in a group may carry it. The single canonical “default role” lives on Group.defaultRoleId.
permissionsstring[]Permission keys granted to this role. Populated by the grant / revoke routes; [] on a freshly-created role.
createdAtstringISO 8601 timestamp.

POST /v1/groups/:id/roles

Creates a new role inside the named group. Writes a role.created audit entry in the same transaction.

Request

{
  "name": "Officer",
  "priority": 80,
  "color": "#ff5050",
  "isDefault": false
}
FieldRequiredDefaultNotes
nameyes1-64 characters. Unique within the group.
priorityyesInteger. Negative values are allowed (treat as “deprioritized”).
colornonullIf present, must match ^#[0-9a-fA-F]{6}$.
isDefaultnofalsePer-role tag. Multiple roles in one group can carry it.

permissions is intentionally not part of the create body; the grant route (POST /v1/roles/:id/permissions, documented below) is the dedicated path for adding permission keys.

Response

201 Created with a Role body. permissions is always [] on a freshly created role.

Audit log

One role.created entry, written in the same transaction:

{
  "groupId": "<groupId>",
  "actorUserId": null,
  "action": "role.created",
  "targetId": "<roleId>",
  "payload": {
    "name": "Officer",
    "priority": 80,
    "color": "#ff5050",
    "isDefault": false
  }
}

Errors

CodeStatusWhen
bad_request400Missing required fields, name out of range, priority not an integer, color not a 7-char hex, or malformed JSON.
not_found404No group with that id in the calling game (soft-deleted and cross-game collapse here).
role_name_taken409Another role in the same group already has that name.
invalid_api_key401API key missing, malformed, or revoked.

GET /v1/groups/:id/roles

Lists every role in a group. Returns a bare array (no pagination wrapper); roles are conventionally a small list (10s, not 1000s).

Path parameters

FieldTypeNotes
idstringThe group id. URL-decoded by the router.

Response

200 OK with a Role[] body. Items are ordered by priority desc with id desc as a tiebreaker, so the highest-authority roles appear first. Permissions are batch-loaded for the page.

Errors

CodeStatusWhen
not_found404No group with that id in the calling game.
invalid_api_key401API key missing, malformed, or revoked.

GET /v1/roles/:id

Fetches a single role by Role.id. Scoped to the calling game (a role whose group belongs to a different game returns 404 to avoid leaking existence). A soft-deleted group also 404s.

Response

200 OK with a Role body.

Errors

CodeStatusWhen
not_found404No role with that id, or the role’s group belongs to a different game, or the role’s group is soft-deleted.
invalid_api_key401API key missing, malformed, or revoked.

PATCH /v1/roles/:id

Updates one or more fields on an existing role. The body is partial: any subset of { name, priority, color, isDefault }. Empty body returns 400.

Each field is diffed per-field against the stored row. Only fields whose new value differs from stored go into the update statement and the audit payload. A no-op PATCH (every supplied value equals the stored one) skips the DB write entirely, returns the unchanged role, and writes no audit entry.

Request

{
  "priority": 90,
  "color": null
}
FieldTypeNotes
namestring1-64 chars. Unique within the group; renaming to an existing name returns 409.
prioritynumberInteger.
colorstring | null7-char hex; pass null to clear.
isDefaultboolean

Response

200 OK with the updated Role body. permissions is loaded fresh.

Audit log

One role.updated entry per non-noop PATCH, with payload: { before, after } containing only the changed fields:

{
  "action": "role.updated",
  "targetId": "<roleId>",
  "payload": {
    "before": { "priority": 80, "color": "#ff5050" },
    "after": { "priority": 90, "color": null }
  }
}

Errors

CodeStatusWhen
bad_request400Empty body, invalid color, name out of range, or priority not an integer.
not_found404Role missing / cross-game / soft-deleted-group.
role_name_taken409Renaming to a name already used by another role in the same group.
invalid_api_key401API key missing, malformed, or revoked.

DELETE /v1/roles/:id

Hard-deletes a role. Roles do not have a soft-delete window.

If any MemberRole rows reference the role, the request returns 409 role_has_members and the role is preserved. The caller must reassign affected members (or remove the assignment) before deleting.

On success, writes a role.deleted audit entry containing the deleted row’s snapshot:

{
  "action": "role.deleted",
  "targetId": "<roleId>",
  "payload": {
    "name": "Officer",
    "priority": 80,
    "color": "#ff5050",
    "isDefault": false
  }
}

Response

204 No Content on success.

Errors

CodeStatusWhen
role_has_members409The role has at least one MemberRole assignment. Reassign first.
not_found404Role missing / cross-game / soft-deleted-group.
invalid_api_key401API key missing, malformed, or revoked.

POST /v1/roles/:id/permissions

Grants a permission key to a role. The route is idempotent: granting a permission the role already has returns the unchanged role with no audit entry and no DB write.

The first time a permission key is granted on a given game, it is auto-registered into PermissionDef (the per-game catalog of “known keys” the dashboard and SDK validators consult). Subsequent grants of the same key reuse the existing PermissionDef row; revoking the key later does not remove the def.

Request

{ "permission": "invite_member" }
FieldRequiredNotes
permissionyes1-128 characters. Free-form string; the dev defines their own keys.

Response

200 OK with the updated Role body. permissions reflects the post-state and is sorted by key.

Audit log

One permission.granted entry per non-noop grant:

{
  "action": "permission.granted",
  "targetId": "<roleId>",
  "payload": {
    "roleId": "<roleId>",
    "permission": "invite_member"
  }
}

Errors

CodeStatusWhen
bad_request400Missing or empty permission, key over 128 characters, or malformed JSON.
not_found404Role missing / cross-game / soft-deleted-group.
invalid_api_key401API key missing, malformed, or revoked.

DELETE /v1/roles/:id/permissions/:permission

Revokes a permission key from a role. The route is idempotent: revoking a permission the role does not have returns the unchanged role with no audit entry. The PermissionDef registry is preserved (revoke does not “forget” the key for the game).

Path parameters

FieldTypeNotes
idstringThe role id. URL-decoded by the router.
permissionstringThe permission key. URL-decoded by the router.

Response

200 OK with the updated Role body. permissions reflects the post-state.

Audit log

One permission.revoked entry per non-noop revoke:

{
  "action": "permission.revoked",
  "targetId": "<roleId>",
  "payload": {
    "roleId": "<roleId>",
    "permission": "invite_member"
  }
}

Errors

CodeStatusWhen
not_found404Role missing / cross-game / soft-deleted-group.
invalid_api_key401API key missing, malformed, or revoked.