Skip to content

Changelog

v2.17 and v3 alpha: Space moderation and access checks

Section titled “v2.17 and v3 alpha: Space moderation and access checks”
  • Operators can moderate spaces through admin endpoints under /admin/spaces. See Admin spaces.
  • getSpace is restricted to the space’s creator, an operator with superadmin, or a caller presenting a space credential.
  • listRepos requires a member or a space credential.
  • listMembers is restricted to the space’s creator and operators with superadmin. See Listing members.

v2.16 — Space Authority, Signed Credentials, and Sync

Section titled “v2.16 — Space Authority, Signed Credentials, and Sync”

Aligns credentials and sync with the latest Proposal 0016 updates.

  • HappyView is the authority for spaces it creates. New spaces use the instance’s service identity DID (did:web or did:plc) as both the space URI DID and authority_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_did controls putMember, removeMember, updateSpace, deleteSpace, and invites. isOwner in listSpaces reflects the creator.
  • Space keys are unique per type across the instance. A second creator using the same type and skey gets 409 Conflict.
  • #atproto_space_host service entry. HappyView publishes an AtprotoSpaceHost entry when a space is created. did:web instances serve it at once. did:plc operators must sync service entries to the PLC directory for it, and the #atproto_space key, to reach their DID document.
  • OAuth scopes must name HappyView as the authority. Apps requesting space: scopes need authority=<HappyView DID> or authority=*. The default authority=self does 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.
  • HTTP Message Signatures replace DPoP for credentials. getSpaceCredential takes the delegation token as Authorization: Bearer, with an RFC 9421 atproto-space signature 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-Space scheme. Requests carry Atproto-Space-Audience and an atproto-space signature over authorization and atproto-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_space key, so other hosts can verify them.
  • Member-list read policies require the read flag. Write-only members and members limited to their own records cannot obtain credentials.
  • Revocation on read loss. Removing a member, or setting their read to false, revokes their credentials. HappyView tells hosts of the space’s native repos through com.atproto.space.notifyCredentialRevoked.
  • The {grant} body and OAuth caller flow on getSpaceCredential
  • dev.happyview.space.getSpaceCredential
  • Verification of credentials issued by other space authorities
  • registerNotify takes a service identifier. {space, service}, where service is a DID with an optional fragment, resolved to its endpoint. Returns {expiresAt}. Unresolvable identifiers fail with ServiceNotResolvable. Registering again replaces the previous registration.
  • Outbound notifyWrite calls. Registered services receive com.atproto.space.notifyWrite once per commit, with service auth from HappyView and {space, repo, repoRev, hash, spaceRev, prevSpaceRev?}. rev repeats repoRev. It is deprecated, still sent in v3 alpha, and removal is not yet scheduled.
  • Inbound notifyWrite from repo hosts. HappyView accepts {space, repo, repoRev, hash} with service auth signed by the writing account, and rev in place of repoRev (deprecated; still accepted in v3 alpha, and removal is not yet scheduled). The writer must pass the write policy. Stale revisions are ignored, and revisions more than 5 minutes in the future fail with FutureRev.
  • Space-wide revision. Every accepted repo update advances a space revision.
  • listRepos serves the writer set. Returns {repos: [{did, repoRev, hash, spaceRev}], cursor?} in space revision order, with limit (default 100, max 1000). cursor is 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 lxm is enforced. Inbound tokens bound to a different method are rejected.

Deprecated. These forms are still served in v3 alpha; removal is not yet scheduled.

  • createSpace and updateSpace accept a single policy or mintPolicy (public, member-list, managing-app) with a sibling managingApp or managingAppDid, applied to both reads and writes when readPolicy and writePolicy are absent.
  • appAccess accepts {"type": "open"} and {"type": "allowList", "allowed": [...]}.
  • com.atproto.simplespace.addMember and dev.happyview.space.addMember accept an access word.
  • registerNotify accepts {space, serviceDid, endpoint} webhook registrations, returning {id, expiresAt}. notifyWrite accepts {space, did, collection, rkey, cid}.
  • spaceType replaces type in simplespace.createSpace and as a new listSpaces filter. type is still accepted in both. It is deprecated, still served in v3 alpha, and removal is not yet scheduled.
  • 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 with CredentialRevoked, and one for another space with InvalidCredential. notifyWrite for an unknown space fails with SpaceNotFound.
  • notifyWrite accepts the bare authority DID as its service-auth audience, as well as #atproto_space_host.
  • config.records_public. It has never been enforced: setting it did not change who could read a space. Use a public readPolicy for a space anyone may read. It is still accepted and stored in v3 alpha; removal is not yet scheduled.
  • space:put_member{did, read, write, is_delegation?} sets a member’s flags and returns {did, read, write}.
  • space:members() entries include read and write booleans beside access.

In v3, scripts use require("happyview.spaces") instead. Its space handle sets a member with set_member{ did, access, is_delegation }.

  • Credential clients must sign requests and use the Atproto-Space scheme.
  • Apps must request space: scopes naming HappyView’s DID as the authority.
  • Syncers must accept notifyWrite calls in place of webhook payloads, or keep a legacy registration. Legacy registrations are deprecated. They are still served in v3 alpha; removal is not yet scheduled.

  • com.atproto.simplespace.getConfig and com.atproto.simplespace.updateConfig. Both were added in v2.10.0.
  • A space repository can move to its author’s PDS. Migration is behind the feature.spaces_pds_migration flag, and starts when the author logs in with a space: scope.
  • Dashboard login drops space: scopes when the account’s PDS does not support spaces.
  • GET /admin/spaces/migration-status reports migration progress to operators. See Space migration status.
  • community.lexicon.service.describe is served.
  • Writes are forwarded to repositories that have moved to their authors’ PDSes.

Aligns with the merged Proposal 0016 specification.

  • ats:// → at:// with space path segment — space URIs now use the standard at:// scheme with a literal space segment: at://<did>/space/<type>/<skey>. Record URIs follow: at://<did>/space/<type>/<skey>/<author>/<collection>/<rkey>.
  • 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)
  • listRepoOps now inlines record values by default via LEFT JOIN against space_records. Pass excludeValues=true for metadata-only responses.
  • SignedCommit.ver — new version field (currently 1) for future-proofing
  • Commit context string now includes the author’s DID: tag || space || author || rev || ikm (was tag || space || rev || ikm)
  • All space URIs now use at:// with a space segment instead of ats://. Clients using the old scheme must update.

Major restructuring to align with atproto Proposal 0016 (Permissioned Data).

  • 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. Deprecated. Still served in v3 alpha; removal is not yet scheduled.
  • Invite endpoints remain under dev.happyview.space.* as HappyView extensions
  • owner_did → authority_did — the DID that controls the space. A separate creator_did tracks who originally created it.
  • accessMode → mintPolicy — controls who can create permissioned repos: member-list (default), public, or managing-app
  • appAllowlist/appDenylist → appAccess — controls third-party app access: open (default) or allowList
  • getMemberGrant → getDelegationToken — renamed and changed from POST to GET. Returns a delegation token (JWT with typ: atproto-space-delegation+jwt, ES256K, 60-second TTL)
  • redeemInvite → acceptInvite — renamed for clarity
  • Space credential typ — changed from space_credential to atproto-space-credential+jwt
  • Space credential TTL — reduced from 4 hours to 2 hours
  • read_self — a new membership access level that restricts reads to only the member’s own records within the space
  • com.atproto.space.getRepoState (GET) — returns per-user repo state including LtHash state and signed commit
  • com.atproto.space.listRepoOps (GET) — returns the record operation log for sync
  • com.atproto.space.listRepos (GET) — lists repos (authors) in a space
  • com.atproto.space.getBlob (GET) — retrieves a blob from a space
  • com.atproto.space.registerNotify (POST) — registers for write notifications
  • com.atproto.space.notifyWrite (POST) — pushes a write notification
  • com.atproto.space.notifySpaceDeleted (POST) — pushes a space-deleted notification
  • com.atproto.simplespace.getConfig (GET) — gets space configuration (mint policy, app access, managing app)
  • com.atproto.simplespace.updateConfig (POST) — updates space configuration
  • 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.
  • New happyview_space_repo_state table — per-user LtHash state + signed commit per space
  • New happyview_space_record_oplog table — ordered record operation log per space
  • New happyview_space_notify_registrations table — write notification registrations
  • Spaces now use authority_did and creator_did instead of owner_did
  • mint_policy and app_access columns replace access_mode, app_allowlist, app_denylist
  • Feature flag disabled response changed from 501 Not Implemented to 404 with FeatureDisabled error code
  • Deleting a space now cascades to all associated data (records, members, repo state, oplog, notifications, credentials)

  • Spaces endpoints now use cursor-based pagination instead of offset-based

  • createRecord: create a record with an auto-generated TID rkey instead of requiring the caller to supply one
  • applyWrites: batch multiple create, update, and delete operations in a single request
  • swapRecord: optional CID-based concurrency guard on putRecord, deleteRecord, and individual operations within applyWrites. Returns 409 Conflict when the record’s current CID doesn’t match.
  • swapCommit: optional revision-based concurrency guard on applyWrites. Asserts the space’s current revision before applying any writes. Returns 409 Conflict on mismatch.
  • Spaces now track a revision field (TID) that advances on every write.
  • Spaces now have their own did field, distinct from the owner_did of 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 did column to the spaces table.
  • Replaced the single getCredential endpoint with a two-step flow:
    1. getMemberGrant: proves membership and returns an HMAC-SHA256 grant (5-minute TTL)
    2. getSpaceCredential: exchanges the grant for an ES256 space credential JWT (4-hour TTL)
  • Removed the refreshCredential endpoint (just repeat the two-step flow)
  • Space credentials are now passed as standard Authorization: Bearer <token> instead of a custom X-Space-Credential header. HappyView distinguishes credentials from other Bearer tokens by checking the JWT typ header (space_credential), matching Dan’s reference implementation.
  • No DPoP auth or client key needed when authenticating via space credential.
  • 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.
  • Fixed WriteOp serde deserialization. swapRecord fields in update and delete operations now correctly deserialize from camelCase JSON.
  • Credential iss claim now uses the space’s DID instead of the owner’s DID.
  • SpaceUri parsing updated to use did (space DID) instead of owner_did.

Released 2026-05-05

Initial release of Permissioned Spaces behind the feature.spaces_enabled experimental flag.

  • 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_deny modes and app allowlists/denylists
  • Space credentials for cross-service read access via X-Space-Credential header
  • 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)