Spaces are containers for permissioned data in atproto. Unlike regular public records that live in a user’s repo, space records are gated: each space’s read and write policies decide who can read or write its data, and by default that is its members.
Concepts
Section titled “Concepts”A space is identified by three components:
- Space DID: the DID of the space’s authority. Spaces created through HappyView use the instance’s service identity DID, or the creator’s DID when the instance publishes no identity. See Space authority.
- Type: the space type as an NSID, describing the modality (e.g. a forum, a group chat, a photo album)
- Space key (skey): a short string differentiating multiple spaces of the same type
These form the space URI: at://<space-did>/space/<type>/<skey>
A space record adds three more components to the URI: the author’s DID, the collection NSID, and the record key:
at://<space-did>/space/<type-nsid>/<skey>/<author-did>/<collection>/<rkey>Feature flag
Section titled “Feature flag”In HappyView, spaces are gated behind the feature.spaces_enabled instance setting. Enable it in the dashboard under System > Experiments or via the admin API:
const response = await fetch("http://127.0.0.1:3000/admin/settings/feature.spaces_enabled", { method: "PUT", headers: { "Authorization": `Bearer ${TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ value: "true" }),});const response = await fetch("http://127.0.0.1:3000/admin/settings/feature.spaces_enabled", { method: "PUT", headers: { "Authorization": `Bearer ${TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ value: "true" }),});let response = client .put("http://127.0.0.1:3000/admin/settings/feature.spaces_enabled") .header("Authorization", format!("Bearer {}", token)) .json(&serde_json::json!({ "value": "true" })) .send() .await?;token := "..." // an admin API key
body := bytes.NewBufferString(`{"value": "true"}`)req, err := http.NewRequest("PUT", "http://127.0.0.1:3000/admin/settings/feature.spaces_enabled", body)if err != nil { log.Fatal(err)}req.Header.Set("Authorization", "Bearer "+token)req.Header.Set("Content-Type", "application/json")resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl -X PUT http://127.0.0.1:3000/admin/settings/feature.spaces_enabled \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"value": "true"}'While the setting is off, every space endpoint returns a 404 error with FeatureDisabled as the error code.
Endpoints
Section titled “Endpoints”Space endpoints are split across two namespaces:
com.atproto.space.*: protocol-level routes (queries, data, credentials, sync)com.atproto.simplespace.*: management routes (create/update/delete spaces, membership)
Some of these endpoints also answer under a deprecated dev.happyview.space.* alias. The invite endpoints exist only under dev.happyview.space.* and are not deprecated. Most endpoints take DPoP authentication or cookie-based session auth. Read and sync endpoints also take a space credential, and notifyWrite and notifySpaceDeleted take service auth.
| Endpoint | Method | Description |
|---|---|---|
com.atproto.simplespace.createSpace | POST | Create a space |
com.atproto.simplespace.getSpace | GET | Get a space and its policies |
com.atproto.space.listSpaces | GET | List spaces by membership |
com.atproto.simplespace.updateSpace | POST | Update a space |
com.atproto.simplespace.deleteSpace | POST | Delete a space |
com.atproto.simplespace.putMember | POST | Add a member or set their access |
com.atproto.simplespace.removeMember | POST | Remove a member |
com.atproto.simplespace.listMembers | GET | List resolved members |
com.atproto.space.createRecord | POST | Create a record (auto-generated rkey) |
com.atproto.space.putRecord | POST | Write a record |
com.atproto.space.getRecord | GET | Get a record |
com.atproto.space.listRecords | GET | List records |
com.atproto.space.deleteRecord | POST | Delete a record |
com.atproto.space.applyWrites | POST | Batch write operations |
com.atproto.space.getLatestCommit | GET | Get per-user signed commit |
com.atproto.space.getRepoState | GET | Earlier name of getLatestCommit |
com.atproto.space.getRepo | GET | Export a user’s repo as a CAR file |
com.atproto.space.listRepoOps | GET | List record operation log entries |
com.atproto.space.listRepos | GET | List the space’s writer set |
com.atproto.space.getBlob | GET | Get a blob from a space |
com.atproto.space.listBlobs | GET | List the blobs a repo’s records reference |
com.atproto.space.getDelegationToken | GET | Get a delegation token (step 1 of credentials) |
com.atproto.space.getSpaceCredential | POST | Exchange it for a space credential (step 2) |
com.atproto.space.registerNotify | POST | Register a syncer for write notifications |
com.atproto.space.unregisterNotify | POST | Withdraw a registration |
com.atproto.space.notifyWrite | POST | Report a repo’s new commit |
com.atproto.space.notifySpaceDeleted | POST | Push a space-deleted notification |
dev.happyview.space.createInvite | POST | Create an invite (HappyView extension) |
dev.happyview.space.acceptInvite | POST | Accept an invite (HappyView extension) |
dev.happyview.space.revokeInvite | POST | Revoke an invite (HappyView extension) |
dev.happyview.space.listInvites | GET | List invites (HappyView extension) |
com.atproto.simplespace.addMember is a deprecated form of putMember. It is still served in v3 alpha; removal is not yet scheduled. See Legacy addMember.
Access model
Section titled “Access model”A space’s creator administers it: they update and delete the space and manage its members and invites.
Instance operators can browse any space for moderation through the admin spaces API without being members. Each read of a space’s contents is recorded in the event log.
Two independent policies decide who can use the space. See Policies.
- The read policy decides who can obtain a space credential to read the whole space.
- The write policy decides whose writes the space tracks and forwards to syncers.
Each policy is member-list (the default), public, or managing-app.
App access controls which third-party apps can obtain credentials: open (the default) or an allow list of OAuth client IDs.
Under member-list policies, each member has two flags, read and write, set independently. The creator is automatically added with both. See Members.
Spaces also support delegation: adding another space as a member, which grants its members access to this space.
Repositories on authors’ PDSes
Section titled “Repositories on authors’ PDSes”A space repository can move from HappyView to its author’s PDS. Each repository goes through three states: polyfill (held by HappyView), migrating, and native (held by the author’s PDS). Migration is gated by the feature.spaces_pds_migration flag. Operators can follow progress with GET /admin/spaces/migration-status.
Alignment with Proposal 0016
Section titled “Alignment with Proposal 0016”HappyView implements atproto Proposal 0016 (Permissioned Data) with some HappyView-specific extensions.
Protocol features implemented
Section titled “Protocol features implemented”- Namespace split:
com.atproto.space.*for protocol routes,com.atproto.simplespace.*for management - Space authority: new spaces use the instance’s DID, published with
#atproto_space_hostand#atproto_spaceentries - Read and write policies:
memberListPolicy,publicPolicy,managingAppPolicy - App access:
open,allowList - Member list:
putMemberwith independentreadandwriteflags - Delegation tokens:
getDelegationToken(GET, 60-second TTL, single use), or tokens signed by the account’s own key - Space credentials:
atproto-space-credential+jwt, ES256, 10-minute TTL, bound to a key withcnf.kidand used with RFC 9421 HTTP Message Signatures - Credential revocation:
notifyCredentialRevokedsent to hosts of repos in the space - Deniable commit signatures: the signature covers a context (space + author + rev + random IKM), not the content hash. HappyView signs the repos it holds with its
#atproto_spacekey - LtHash: homomorphic set-hash (2048-byte state, 1024 uint16 lanes, BLAKE3 XOF)
- SignedCommit: versioned commit struct (
ver: 1) with hash, ikm, sig, mac, rev - Record operation log:
listRepoOpsreturns the oplog for sync (values inlined by default,excludeValuesto opt out) - Latest commit:
getLatestCommitreturns the signed commit for a user in a space - Repo export:
getRepoexports a user’s repo as a CAR v1 file (signedCommit + DRISL index) - Sync:
registerNotifyby service identifier,notifyWritein both directions,listReposfrom the writer set with a space-wide revision - Space-scoped blobs:
getBlob,listBlobs
HappyView extensions (not in the protocol spec)
Section titled “HappyView extensions (not in the protocol spec)”- Invite system:
createInvite,acceptInvite,revokeInvite,listInvites(underdev.happyview.space.*) isDelegationon members: allows spaces to be members of other spacesdisplayName,descriptionon spaces: human-readable metadataconfigobject:membership_public, a setting stored with the space, plus arbitrary extra fields.records_publicis deprecated.read_selfaccess: limits a member’s reads to their own records
Next steps
Section titled “Next steps”- Managing spaces: create, update, and delete spaces
- Members: manage membership and delegation
- Records: read and write permissioned data
- Credentials: cross-service authentication for spaces
- Write notifications: keep syncers up to date
- Invites: invite-based membership
- Changelog: version history
© HappyProto

