Members
The member list records who can read and write within a space. Each member has two independent flags:
read: the member can read the space's records, and can obtain space credentials under a member-list read policy.write: the member can write records into the space, and is admitted as a writer under a member-list write policy.
A member can hold either flag, both, or neither. The space's policies decide whether the member list is consulted at all.
Only the space's creator or a HappyView super admin can change the member list.
Setting a member
com.atproto.simplespace.putMember adds a member, or replaces the flags of an existing member. Both flags are required, so a call never grants or withdraws access by default.
const response = await fetch("https://happyview.example.com/xrpc/com.atproto.simplespace.putMember", {
method: "POST",
headers: {
"X-Client-Key": CLIENT_KEY,
"Authorization": `DPoP ${ACCESS_TOKEN}`,
"DPoP": DPOP_PROOF,
"Content-Type": "application/json",
},
body: JSON.stringify({
space: "at://did:web:happyview.example.com/space/com.example.forum/main",
did: "did:plc:newmember",
read: true,
write: true,
}),
});
interface Member {
id: string;
space_id: string;
did: string;
access: { read: boolean; write: boolean };
is_delegation: boolean;
granted_by: string | null;
created_at: string;
}
const data: { member: Member } = await response.json();Input:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
space | string | Yes | The space URI | |
did | string | Yes | DID of the member, or a space URI for delegation | |
read | boolean | Yes | Whether the member can read the space | |
write | boolean | Yes | Whether the member can write to the space | |
isDelegation | boolean | No | false | Whether this member is a delegated space (HappyView extension) |
Response (201):
{
"member": {
"id": "0b7f6a52-4f0e-4d1e-9f3c-2a8f1e6d9c41",
"space_id": "5d1c8e0a-7b2f-4c39-8e61-3f4a9b2d7e10",
"did": "did:plc:newmember",
"access": { "read": true, "write": true },
"is_delegation": false,
"granted_by": "did:plc:creator123",
"created_at": "2026-09-30T12:00:00Z"
}
}Setting read to false revokes the member's outstanding space credentials. See Revocation.
Members limited to reading their own records keep that limit when putMember updates them. The limit is set with the read_self access word, through legacy addMember or the Lua API.
Removing a member
Removing a member also revokes their outstanding space credentials. Removing a DID that is not a member fails with 404.
const response = await fetch("https://happyview.example.com/xrpc/com.atproto.simplespace.removeMember", {
method: "POST",
headers: {
"X-Client-Key": CLIENT_KEY,
"Authorization": `DPoP ${ACCESS_TOKEN}`,
"DPoP": DPOP_PROOF,
"Content-Type": "application/json",
},
body: JSON.stringify({
space: "at://did:web:happyview.example.com/space/com.example.forum/main",
did: "did:plc:newmember",
}),
});Response (200):
{
"success": true
}Listing members
const response = await fetch(
"https://happyview.example.com/xrpc/com.atproto.simplespace.listMembers?space=at://did:web:happyview.example.com/space/com.example.forum/main",
{
headers: {
"X-Client-Key": CLIENT_KEY,
"Authorization": `DPoP ${ACCESS_TOKEN}`,
"DPoP": DPOP_PROOF,
},
},
);
interface ResolvedMember {
did: string;
read: boolean;
write: boolean;
}
const data: { members: ResolvedMember[] } = await response.json();If the space's membership_public config is true, this endpoint is accessible without authentication. Otherwise, the caller must be authenticated and be a member.
The response returns the resolved member list, with delegation chains traversed and flattened:
{
"members": [
{ "did": "did:plc:creator123", "read": true, "write": true },
{ "did": "did:plc:delegated-user", "read": true, "write": false },
{ "did": "did:plc:newmember", "read": true, "write": true }
]
}Delegation
A space can be added as a member of another space by setting isDelegation: true and passing the delegated space's URI as did. The delegated space's members become members of this space, with the flags they hold in the delegated space.
const response = await fetch("https://happyview.example.com/xrpc/com.atproto.simplespace.putMember", {
method: "POST",
headers: {
"X-Client-Key": CLIENT_KEY,
"Authorization": `DPoP ${ACCESS_TOKEN}`,
"DPoP": DPOP_PROOF,
"Content-Type": "application/json",
},
body: JSON.stringify({
space: "at://did:web:happyview.example.com/space/com.example.forum/main",
did: "at://did:web:happyview.example.com/space/com.example.team/engineering",
read: true,
write: false,
isDelegation: true,
}),
});Delegation chains are resolved up to 10 levels deep. When a user appears in several chains, each flag is combined separately: the user can read if any path grants read, and can write if any path grants write.
Example: nested teams
In this example, the forum's resolved member list contains:
- Alice with
readandwrite(write from Engineering) - Bob with
readandwrite(via Engineering) - Carol with
read(via Design)
Legacy addMember
com.atproto.simplespace.addMember and dev.happyview.space.addMember are deprecated and kept until v3. They take a single access word in place of the two flags:
access | Equivalent |
|---|---|
write | read: true, write: true |
read (default) | read: true, write: false |
read_self | Reads limited to the member's own records |
none | read: false, write: false |
addMember fails with 409 Conflict when the DID is already a member. Use putMember to change an existing member.