Collaboration
Members and roles
A space's members list is the one call that answers "who is here, what may they do, and what do they look like". Every row carries the member's permission, their status on the ACL, and their profile.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/spaces/:spaceId/members |
full roster |
| GET | /v1/spaces/:spaceId/members/me |
the caller's own row |
| GET | /v1/spaces/:spaceId/members/requests |
pending join requests |
| GET | /v1/spaces/:spaceId/members/subscribe |
live membership changes over SSE |
| GET | /v1/spaces/:spaceId/members/:identity |
one member by account id |
curl -s http://127.0.0.1:7001/v1/spaces/$SPACE/members
{ "members": [
{ "identity": "A6ux…",
"permission": "owner",
"status": "active",
"name": "Alice",
"iconCid": "…" } ] }
| Field | Meaning |
|---|---|
identity |
account id |
permission |
the role — see the table below |
status |
the member's ACL state |
name, iconCid |
profile, resolved from the same cache that feeds the identities directory; join-time metadata is the always-present baseline |
requestRecordId |
present only on a pending-request entry — pass it to POST …/acl/accept |
any members list <spaceId>
any members me <spaceId>
any members get <spaceId> <identity>
any members requests <spaceId>
any members subscribe <spaceId>
Permission strings
| Wire string | Meaning |
|---|---|
none |
no access |
reader |
read only |
guest |
read only through the space's shared public guest key |
writer |
read and write content |
admin |
writer plus member management |
owner |
everything, including ownership transfer and stop-sharing |
Status strings
| Wire string | Meaning |
|---|---|
unknown |
not resolvable from ACL state |
joining |
join request posted, awaiting the owner's accept |
active |
a current member |
removed |
removed by an owner/admin |
declined |
join request declined |
removing |
self-removal requested |
canceled |
join request canceled by the requester |
Unknown values sent on the wire (for example in an ACL body) return 400 request.schema.
Your own role from the space list
SpaceInfo.ownRole on GET /v1/spaces[/:id] mirrors the caller's own permission from ACL state — one pass when the space loads, then one per applied ACL record, so a demotion by another peer lands as a row update. The raw rows on POST /v1/spaces/query/subscribe stream role changes live, which lets a UI gate role-dependent controls straight from the space list without a per-space members/me fan-out.
Note.
ownRole: "none"doubles as "not mirrored yet" — a space this device has not loaded, a pending join, a tombstoned row. Treat it as unknown / no access, and readGET …/members/mewhen the answer matters. On a one-to-one space both participants reportwriter, neverowner, because the ACL owner slot is a synthetic key nobody holds.
Live membership changes
GET /v1/spaces/:spaceId/members/subscribe streams new members, permission or status flips, and removals:
event: ready
data: {}
event: member
data: { "kind": "added" | "changed" | "removed",
"member": { …Member… },
"previous": { …Member… } | null }
event: lagged
data: { "total": 3 }
event: closed
data: { "reason": "server_shutdown" }
member always carries the full post-event row (same shape as GET …/members/:identity); previous is null on added. The forwarder is a small buffered channel — on lagged, re-GET the roster. The envelope and closed reasons are shared with the other callback streams.
Roster vs directory
Two surfaces resolve "who is this person" and answer different questions:
| Need | Surface |
|---|---|
| A members panel with avatars and roles for one space | GET /v1/spaces/:id/members — authoritative for rights |
A name/icon for any account id you hold (a chat creator, a mention) |
GET /v1/identities[/:identity] — account-global, carries no rights |
There is no cross-space role rollup: to show someone's role in each shared space, read each space's roster.
Why it matters. Rights are not a column a server consults; they are what the ACL cryptographically grants. The roster you read is a projection of the same signed records every peer verifies, so no member can see a different truth than another.
Profiles are encrypted and resolve only after a contact's key arrives — rows may carry an empty name. See Profile for the receiving-side rules.