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
| OperationID | Method + Path | RBAC | Notes |
|---|---|---|---|
list-members | POST /members/list | — (any org member) | query search + pagination; used by chat member-search too |
update-member-role | POST /members/{id}/update | EditUser | was /memberships/update-user; role in body |
remove-member | POST /members/{id}/remove | BanUser | was /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-roleaccepts the assignable subset (typicallymember | moderator | admin).presenceStatus=offline | online | away | busy— replaces the numericdbmodels.PresenceStatus(int160/1/2) leak with a self-describing string enum (C6). The manual-override status is internal; the wire exposes the single effectivepresenceStatus.
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)
membershipkeepslist+update-role+remove(invitations →invitationmodule, task #5).- Map
dbmodels.PresenceStatus(int16) → the string enum at the wire boundary. - Stringify member
id; dropworkosId. - Move the duplicated
PaginationMaxLimit/DefaultLimitconsts (also inorganization) into a shared package — one definition.