Skip to main content

Resource: Members

Tag: Members · Module: membership (shrunk — invitations were carved out to invitation)

Organization members (the users who belong to an org). After the invitation extraction (task #5), the membership module keeps only member listing + role/removal.

Grounded: membership/api/{listmembers,updaterole,removemember}.go, membership/models/types.go, internal/domain/database/user_presence.go (PresenceStatus), internal/permissions/models.go (roles).


Operations

OperationIDMethod + PathRBACNotes
list-membersPOST /members/list— (any org member)query search + pagination; used by chat member-search too
update-member-rolePOST /members/{id}/updateEditUserwas /memberships/update-user; role in body
remove-memberPOST /members/{id}/removeBanUserwas /memberships/remove-user; optional live-kick

Paths move /memberships/*/members/*. {id} = the member's user id.


Identifier — stringified internal user id

A member is a user in the org. The wire id is the stringified internal user id (C2 — int64 has no non-vendor opaque alternative; exposing the WorkOS user id would leak the vendor). This is the canonical user-id representation across the whole contract — the same id appears in Space.occupantUserIds, recording participants, etc. (WorkOS user ids are never exposed.)


DTOs

Member

{
"id": "string", // stringified internal user id
"displayName": "string",
"avatarUrl": "string",
"email": "string",
"role": "guest | member | moderator | admin", // C6 — wire enum (permissions/models.go)
"presenceStatus": "offline | online | away | busy" // C6 — string enum (was numeric int16 0/1/2)
}
// Dropped: workosId — vendor id, unused by the FE (it keys identity/presence on `id`).

list-members

// Request
{
"query?": "string", // C8 — optional, case-insensitive substring on displayName OR email
"limit": 20, // 1..100, default 20
"offset": 0
}
// Response — generic envelope (C4)
{ "data": [ /* Member */ ], "page": { "limit": 20, "offset": 0, "total": 58 } }

query replaces the current user field; same free-text search convention as the other list endpoints.

update-member-role (POST /members/{id}/update)

// Request — role in the body (bare `/update`; the member is identified by the path)
{ "role": "member | moderator | admin" } // the target role (was `role`/RoleSlug)
// Response — the updated Member
{ /* Member */ }

remove-member ({id} in path)

// Request
{ "spaceId?": "string" } // C8 — optional, the space whose live realtime session to kick from (user may be offline)
// Response — 204 No Content

Enums (wire)

  • role = guest | member | moderator | admin (permissions/models.go:14-17). update-member-role accepts the assignable subset (typically member | moderator | admin).
  • presenceStatus = offline | online | away | busy — replaces the numeric dbmodels.PresenceStatus (int16 0/1/2) leak with a self-describing string enum (C6). The manual-override status is internal; the wire exposes the single effective presenceStatus.

Scope / extensibility

List / update-role / remove are the core ops. Members is expected to grow (e.g. get-member, suspend/unsuspend, member activity); the /members/{id}/… paths + Members tag absorb new ops without restructuring. Not pre-built (YAGNI). The {data, page} envelope is the shared list shape (one reusable Page<T> type) used by every list endpoint — kept here too.

Module (backend)

  • membership keeps list + update-role + remove (invitations → invitation module, task #5).
  • Map dbmodels.PresenceStatus (int16) → the string enum at the wire boundary.
  • Stringify member id; drop workosId.
  • Move the duplicated PaginationMaxLimit/DefaultLimit consts (also in organization) into a shared package — one definition.