Permissioned Spaces

Members

Membership determines who can read and write within a space. Members have either read or write access — write implies read.

Adding a member

Only the space owner or a super admin can add members.

const response = await fetch("https://happyview.example.com/xrpc/dev.happyview.space.addMember", {
  method: "POST",
  headers: {
    "X-Client-Key": CLIENT_KEY,
    "Authorization": `DPoP ${ACCESS_TOKEN}`,
    "DPoP": DPOP_PROOF,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    space: "ats://did:plc:abc123/com.example.forum/main",
    did: "did:plc:newmember",
    access: "write",
    isDelegation: false,
  }),
});
interface Member {
  id: string;
  spaceId: string;
  did: string;
  access: string;
  isDelegation: boolean;
  grantedBy: string;
  createdAt: string;
}
const data: { member: Member } = await response.json();

Input:

FieldTypeRequiredDefaultDescription
spacestringYesThe space to add the member to
didstringYesDID of the member (or space for delegation)
accessstringNoreadread or write
isDelegationbooleanNofalseWhether this member is a delegated space

Response (201):

{
  "member": {
    "id": "uuid",
    "spaceId": "space-uuid",
    "did": "did:plc:newmember",
    "access": "write",
    "isDelegation": false,
    "grantedBy": "did:plc:abc123",
    "createdAt": "2026-05-09T12:00:00Z"
  }
}

Removing a member

const response = await fetch("https://happyview.example.com/xrpc/dev.happyview.space.removeMember", {
  method: "POST",
  headers: {
    "X-Client-Key": CLIENT_KEY,
    "Authorization": `DPoP ${ACCESS_TOKEN}`,
    "DPoP": DPOP_PROOF,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    space: "ats://did:plc:abc123/com.example.forum/main",
    did: "did:plc:newmember",
  }),
});

Listing members

const response = await fetch(
  "https://happyview.example.com/xrpc/dev.happyview.space.listMembers?space=ats://did:plc:abc123/com.example.forum/main",
  {
    headers: {
      "X-Client-Key": CLIENT_KEY,
      "Authorization": `DPoP ${ACCESS_TOKEN}`,
      "DPoP": DPOP_PROOF,
    },
  },
);
interface ResolvedMember {
  did: string;
  access: string;
}
const data: { members: ResolvedMember[] } = await response.json();

If the space's membershipPublic 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 — delegation chains are traversed and flattened:

{
  "members": [
    { "did": "did:plc:abc123", "access": "write" },
    { "did": "did:plc:newmember", "access": "write" },
    { "did": "did:plc:delegated-user", "access": "read" }
  ]
}

Delegation

A space can be added as a member of another space by setting isDelegation: true. This transitively grants access to all members of the delegated space.

const response = await fetch("https://happyview.example.com/xrpc/dev.happyview.space.addMember", {
  method: "POST",
  headers: {
    "X-Client-Key": CLIENT_KEY,
    "Authorization": `DPoP ${ACCESS_TOKEN}`,
    "DPoP": DPOP_PROOF,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    space: "ats://did:plc:abc123/com.example.forum/main",
    did: "ats://did:plc:org/com.example.team/engineering",
    access: "read",
    isDelegation: true,
  }),
});

Delegation chains are resolved up to 10 levels deep. When a user appears in multiple chains, the highest access level wins (write > read).

Example: nested teams

In this example:

  • Alice has write access (via Engineering)
  • Bob has write access (via Engineering)
  • Carol has read access (via Design)
  • Alice also appears in Design, but write wins over read