Permissioned Spaces
Changelog
v2.16 — Space Authority, Signed Credentials, and Sync
Aligns credentials and sync with the latest Proposal 0016 updates.
Space authority
- HappyView is the authority for spaces it creates. New spaces use the instance's service identity DID (
did:webordid:plc) as both the space URI DID andauthority_did:at://<instance DID>/space/<type>/<skey>. Instances without a published service identity use the creator's DID, as before. - The creator administers the space.
creator_didcontrolsputMember,removeMember,updateSpace,deleteSpace, and invites.isOwnerinlistSpacesreflects the creator. - Space keys are unique per type across the instance. A second creator using the same type and
skeygets409 Conflict. #atproto_space_hostservice entry. HappyView publishes anAtprotoSpaceHostentry when a space is created.did:webinstances serve it at once.did:plcoperators must sync service entries to the PLC directory for it, and the#atproto_spacekey, to reach their DID document.- OAuth scopes must name HappyView as the authority. Apps requesting
space:scopes needauthority=<HappyView DID>orauthority=*. The defaultauthority=selfdoes not cover HappyView-authority spaces. - Managing apps are reached through the service entry their identifier names (e.g.
did:web:forum.example.com#forum), not only#atproto_pds.
Credentials
- HTTP Message Signatures replace DPoP for credentials.
getSpaceCredentialtakes the delegation token asAuthorization: Bearer, with an RFC 9421atproto-spacesignature by a fresh P-256 key. The body is{space, clientAttestation?}. No OAuth session is needed. - Credentials are bound to that key through
cnf.kid. - Credentials use the
Atproto-Spacescheme. Requests carryAtproto-Space-Audienceand anatproto-spacesignature overauthorizationandatproto-space-audience. The audience is the repo DID for repo reads and the authority DID for space-wide calls. Bearer space credentials are refused. - New error:
BadSpaceSignature(401). - Credentials last 10 minutes (was 2 hours).
- Delegation tokens are single-use. A second exchange fails with
InvalidDelegationToken. Tokens signed by the account's own key, as a spaces-capable PDS issues, are accepted beside HappyView's own. - Credentials for HappyView-authority spaces are signed with the instance's
#atproto_spacekey, so other hosts can verify them. - Member-list read policies require the
readflag. Write-only members and members limited to their own records cannot obtain credentials. - Revocation on read loss. Removing a member, or setting their
readtofalse, revokes their credentials. HappyView tells hosts of the space's native repos throughcom.atproto.space.notifyCredentialRevoked.
Removed
- The
{grant}body and OAuth caller flow ongetSpaceCredential dev.happyview.space.getSpaceCredential- Verification of credentials issued by other space authorities
Sync
registerNotifytakes a service identifier.{space, service}, whereserviceis a DID with an optional fragment, resolved to its endpoint. Returns{expiresAt}. Unresolvable identifiers fail withServiceNotResolvable. Registering again replaces the previous registration.- Outbound
notifyWritecalls. Registered services receivecom.atproto.space.notifyWriteonce per commit, with service auth from HappyView and{space, repo, repoRev, hash, spaceRev, prevSpaceRev?}.revrepeatsrepoRevuntil v3. - Inbound
notifyWritefrom repo hosts. HappyView accepts{space, repo, repoRev, hash}with service auth signed by the writing account, andrevin place ofrepoRevuntil v3. The writer must pass the write policy. Stale revisions are ignored, and revisions more than 5 minutes in the future fail withFutureRev. - Space-wide revision. Every accepted repo update advances a space revision.
listReposserves the writer set. Returns{repos: [{did, repoRev, hash, spaceRev}], cursor?}in space revision order, withlimit(default 100, max 1000).cursoris a space revision: pass the last one processed to list only repos updated after it.- Fallback sweep. HappyView re-syncs repos hosted on their authors' PDSes every 5 minutes.
- Service auth
lxmis enforced. Inbound tokens bound to a different method are rejected.
Backward compatibility (until v3)
createSpaceandupdateSpaceaccept a singlepolicyormintPolicy(public,member-list,managing-app) with a siblingmanagingAppormanagingAppDid, applied to both reads and writes whenreadPolicyandwritePolicyare absent.appAccessaccepts{"type": "open"}and{"type": "allowList", "allowed": [...]}.com.atproto.simplespace.addMemberanddev.happyview.space.addMemberaccept anaccessword.registerNotifyaccepts{space, serviceDid, endpoint}webhook registrations, returning{id, expiresAt}.notifyWriteaccepts{space, did, collection, rkey, cid}.
Request field renames
spaceTypereplacestypeinsimplespace.createSpaceand as a newlistSpacesfilter.typeis accepted in both until v3.
Credential and notification errors
- Credential lifetime is enforced. Credentials lasting more than an hour, without a
jti, or issued more than 5 seconds in the future are refused. - Named errors. A credential addressed to the wrong DID fails with
BadSpaceAudience, a revoked one withCredentialRevoked, and one for another space withInvalidCredential.notifyWritefor an unknown space fails withSpaceNotFound. notifyWriteaccepts the bare authority DID as its service-auth audience, as well as#atproto_space_host.
Deprecated
config.records_public. It has never been enforced: setting it did not change who could read a space. Use apublicreadPolicyfor a space anyone may read. It is still accepted and stored until v3.
Lua
space:put_member{did, read, write, is_delegation?}sets a member's flags and returns{did, read, write}.space:members()entries includereadandwritebooleans besideaccess.
Breaking changes
- Credential clients must sign requests and use the
Atproto-Spacescheme. - Apps must request
space:scopes naming HappyView's DID as the authority. - Syncers must accept
notifyWritecalls in place of webhook payloads, or keep a legacy registration until v3.
v2.11 — Final Proposal 0016 Alignment
Aligns with the merged Proposal 0016 specification.
URI scheme change
ats://→at://withspacepath segment — space URIs now use the standardat://scheme with a literalspacesegment:at://<did>/space/<type>/<skey>. Record URIs follow:at://<did>/space/<type>/<skey>/<author>/<collection>/<rkey>.
Endpoint changes
getRepoState→getLatestCommit— renamed to match the proposal. The old name is kept as a backward-compatible alias.getRepo(GET) — new endpoint that exports a user's repo as a CAR v1 file (two roots: signedCommit + DRISL index)listRepoOpsnow inlines record values by default via LEFT JOIN againstspace_records. PassexcludeValues=truefor metadata-only responses.
Commit changes
SignedCommit.ver— new version field (currently1) for future-proofing- Commit context string now includes the author's DID:
tag || space || author || rev || ikm(wastag || space || rev || ikm)
Breaking changes
- All space URIs now use
at://with aspacesegment instead ofats://. Clients using the old scheme must update.
v2.10.0 — Proposal 0016 Alignment
Major restructuring to align with atproto Proposal 0016 (Permissioned Data).
Namespace split
- Protocol routes now live under
com.atproto.space.*(queries, data, credentials) - Management routes now live under
com.atproto.simplespace.*(create/update/delete spaces, membership, config) dev.happyview.space.*endpoints remain as backward-compatible aliases until v3- Invite endpoints remain under
dev.happyview.space.*as HappyView extensions
New terminology
owner_did→authority_did— the DID that controls the space. A separatecreator_didtracks who originally created it.accessMode→mintPolicy— controls who can create permissioned repos:member-list(default),public, ormanaging-appappAllowlist/appDenylist→appAccess— controls third-party app access:open(default) orallowListgetMemberGrant→getDelegationToken— renamed and changed from POST to GET. Returns a delegation token (JWT withtyp: atproto-space-delegation+jwt, ES256K, 60-second TTL)redeemInvite→acceptInvite— renamed for clarity- Space credential
typ— changed fromspace_credentialtoatproto-space-credential+jwt - Space credential TTL — reduced from 4 hours to 2 hours
New access level
read_self— a new membership access level that restricts reads to only the member's own records within the space
New endpoints
com.atproto.space.getRepoState(GET) — returns per-user repo state including LtHash state and signed commitcom.atproto.space.listRepoOps(GET) — returns the record operation log for synccom.atproto.space.listRepos(GET) — lists repos (authors) in a spacecom.atproto.space.getBlob(GET) — retrieves a blob from a spacecom.atproto.space.registerNotify(POST) — registers for write notificationscom.atproto.space.notifyWrite(POST) — pushes a write notificationcom.atproto.space.notifySpaceDeleted(POST) — pushes a space-deleted notificationcom.atproto.simplespace.getConfig(GET) — gets space configuration (mint policy, app access, managing app)com.atproto.simplespace.updateConfig(POST) — updates space configuration
Cryptographic primitives
- LtHash — homomorphic set-hash for per-user repo state. 2048-byte state with 1024 little-endian uint16 lanes using BLAKE3 XOF. Supports insert/remove operations for incremental record tracking.
- Deniable commit signatures — users sign context (space DID + rev + random IKM) rather than content hash, producing a MAC that proves authorship without binding the user to specific content.
Data model changes
- New
happyview_space_repo_statetable — per-user LtHash state + signed commit per space - New
happyview_space_record_oplogtable — ordered record operation log per space - New
happyview_space_notify_registrationstable — write notification registrations - Spaces now use
authority_didandcreator_didinstead ofowner_did mint_policyandapp_accesscolumns replaceaccess_mode,app_allowlist,app_denylist
Breaking changes
- Feature flag disabled response changed from
501 Not Implementedto404withFeatureDisablederror code - Deleting a space now cascades to all associated data (records, members, repo state, oplog, notifications, credentials)
v2.8.0
Bug fixes
- Spaces endpoints now use cursor-based pagination instead of offset-based
v2.6.0
New endpoints
createRecord: create a record with an auto-generated TID rkey instead of requiring the caller to supply oneapplyWrites: batch multiple create, update, and delete operations in a single request
Optimistic concurrency
swapRecord: optional CID-based concurrency guard onputRecord,deleteRecord, and individual operations withinapplyWrites. Returns409 Conflictwhen the record's current CID doesn't match.swapCommit: optional revision-based concurrency guard onapplyWrites. Asserts the space's current revision before applying any writes. Returns409 Conflicton mismatch.- Spaces now track a
revisionfield (TID) that advances on every write.
Space DID separation
- Spaces now have their own
didfield, distinct from theowner_didof the space creator. For personal spaces these are the same DID; multi-party spaces will have their own DID. - All URI construction and lookups use the space's DID. Ownership checks use
owner_did. - New database migration adds the
didcolumn to thespacestable.
Two-step credential flow
- Replaced the single
getCredentialendpoint with a two-step flow:getMemberGrant: proves membership and returns an HMAC-SHA256 grant (5-minute TTL)getSpaceCredential: exchanges the grant for an ES256 space credential JWT (4-hour TTL)
- Removed the
refreshCredentialendpoint (just repeat the two-step flow)
Bearer auth for space credentials
- Space credentials are now passed as standard
Authorization: Bearer <token>instead of a customX-Space-Credentialheader. HappyView distinguishes credentials from other Bearer tokens by checking the JWTtypheader (space_credential), matching Dan's reference implementation. - No DPoP auth or client key needed when authenticating via space credential.
Endpoint naming
- Space CRUD endpoints renamed to verbNoun format:
space.create→space.createSpace,space.get→space.getSpace,space.list→space.listSpaces,space.update→space.updateSpace,space.delete→space.deleteSpace. - Invite endpoints moved out of the
invite.*sub-namespace:invite.create→space.createInvite,invite.redeem→space.redeemInvite,invite.revoke→space.revokeInvite,invite.list→space.listInvites. - Old endpoint names are still available as legacy aliases and will be removed in a future release.
Bug fixes
- Fixed
WriteOpserde deserialization.swapRecordfields inupdateanddeleteoperations now correctly deserialize from camelCase JSON. - Credential
issclaim now uses the space's DID instead of the owner's DID. SpaceUriparsing updated to usedid(space DID) instead ofowner_did.
v2.5.0
Released 2026-05-05
Initial release of Permissioned Spaces behind the feature.spaces_enabled experimental flag.
Features
- Space CRUD:
create,get,list,update,delete - Record operations:
putRecord,getRecord,listRecords,deleteRecord - Membership management:
addMember,removeMember,listMembers - Invite system:
invite.create,invite.redeem,invite.revoke,invite.list ats://URI scheme for addressing permissioned data- Access model with
default_allow/default_denymodes and app allowlists/denylists - Space credentials for cross-service read access via
X-Space-Credentialheader - Delegation: adding a space as a member transitively grants access to its members
- Lua scripting context includes space metadata (
space.did,space.owner_did,space.type_nsid,space.skey)