Collaboration
ACL
Every membership change is a signed record appended to the space's access-control list and verified by every peer. These endpoints write those records; the members list is how you read the result.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/spaces/:spaceId/acl/accept |
grant a pending join request a permission |
| POST | /v1/spaces/:spaceId/acl/decline |
decline a join request |
| POST | /v1/spaces/:spaceId/acl/permissions |
change permissions, batched |
| POST | /v1/spaces/:spaceId/acl/remove |
remove accounts — rotates the read key |
| POST | /v1/spaces/:spaceId/acl/add |
add accounts by identity |
| POST | /v1/spaces/:spaceId/acl/ownership |
transfer ownership |
| POST | /v1/spaces/:spaceId/acl/self-remove |
leave the space |
| POST | /v1/spaces/:spaceId/acl/cancel-join |
withdraw your own join request |
| POST | /v1/spaces/:spaceId/acl/stop-sharing |
drop everyone and rotate the key |
Every successful operation returns 204 No Content. self-remove, cancel-join and stop-sharing take no body. Unknown permission strings return 400 request.schema.
Accepting a join request
Pending requests appear on GET /v1/spaces/:spaceId/members/requests; each row carries a requestRecordId.
curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/acl/accept \
-d '{"requestRecordId": "bafy…", "permission": "writer"}'
curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/acl/decline \
-d '{"identity": "A6ux…"}'
any acl accept <spaceId>
any acl decline <spaceId>
On the joiner's side the members row flips from joining to active (or declined).
Changing permissions
curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/acl/permissions \
-d '{"changes": [
{"identity": "A6ux…", "permission": "reader"},
{"identity": "BcdE…", "permission": "admin"}]}'
any acl grant <spaceId> <identity> <permission>
Members see their own change on SpaceInfo.ownRole and on the members stream as a changed frame.
Removing members
curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/acl/remove \
-d '{"identities": ["A6ux…", "BcdE…"]}'
any acl remove <spaceId> <identity>...
Removal rotates the space's read key. Content written after the rotation is encrypted under a key the removed accounts never receive.
Why it matters. Removal is enforced by cryptography rather than by a server refusing reads: what a removed member keeps is the ciphertext they already had, and the sync nodes cannot hand them anything newer they could decrypt.
Adding by identity
When you hold the account id, add directly instead of passing an invite token:
curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/acl/add \
-d '{"accounts": [
{"identity": "A6ux…", "permission": "writer", "metadata": {"name": "Alice"}}]}'
any acl add <spaceId> <identity>[,<identity>...] <permission>
The whole batch lands in one ACL record, and every added account is notified through the coordinator inbox (durable, retried). On their side the space shows up as a synced invite_pending row in the space list, carrying the sender-supplied name hint. They are already full ACL members; the pending state only governs whether their device materializes the space — nothing is downloaded until accepted.
GET /v1/spaces?status=invite_pending # discover
POST /v1/spaces/:spaceId/invite/accept # 200 SpaceInfo, or 202 while loading continues in the background
POST /v1/spaces/:spaceId/invite/decline # 204 — synced sticky, non-terminal
Accept flips the synced status to active on every device and loads the space; it is idempotent and overrides an earlier decline. Decline suppresses the invite account-wide but changes nothing on the ACL — the account remains a member until it self-removes.
| Status | Code | When |
|---|---|---|
| 404 | space.not_found |
unknown space id |
| 409 | space.not_invite_pending |
the row is not awaiting approval |
| 400 | request.invalid_field |
a one-to-one row — use the one-to-one endpoints |
Ownership and leaving
curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/acl/ownership \
-d '{"newOwner": "A6ux…", "oldOwnerPerm": "admin"}'
curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/acl/self-remove
curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/acl/cancel-join
curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/acl/stop-sharing
any acl ownership <spaceId>
any acl self-remove <spaceId>
any acl cancel-join <spaceId>
any acl stop-sharing <spaceId>
| Operation | Effect |
|---|---|
ownership |
hands the owner role to newOwner; the previous owner keeps oldOwnerPerm |
self-remove |
requests your own removal; your row shows removing until applied |
cancel-join |
withdraws a pending join request; row shows canceled |
stop-sharing |
drops every other member and rotates the read key — the space becomes private to the owner again |
Note. A one-to-one space has an immutable two-writer ACL, so none of these operations apply to it. "Accepting" a direct chat is a device-local decision, described on One-to-one.