atproto pds in zig pds.zat.dev
pds atproto
zds docs permissioned-data-proposal-94.md
12 kB

permissioned data proposal 94 notes #

Historical design notes: credential and membership examples below predate the October 1, 2026 alpha. Use permissioned data for the current ZDS contract.

These notes track Bluesky proposal PR #94 from the perspective of ZDS. They are not implementation instructions and should not be treated as a stable protocol contract. The proposal is a draft; use this page to reduce wrong-way risk while keeping ZDS's current permissioned-data work clearly experimental.

PR: https://github.com/bluesky-social/proposals/pull/94

working posture #

The original exploration treated this surface as a sandcastle without external production users. That assumption no longer describes the deployment: consumers need coordinated migrations for breaking changes. Avoid deepening older experimental shapes when the proposal has already moved away from them.

Prefer small alignment work that preserves optionality:

  • keep the API reference and operator docs explicit that this is experimental
  • isolate prototype code under the existing space and permissioned-data modules
  • add tests around durable semantics that are likely to survive
  • avoid adding new member-list behavior or resurrecting old credential names

ZDS's immediate goal is modest: get ahead of the likely direction enough to store private data on the user's PDS, while keeping the implementation easy to reshape. Do not write code or docs as if the draft is settled.

recent design pressure #

On 2026-06-24, Richard Barnes pushed back publicly on the proposal's broad scope. His useful objection is that "permissioned data" appears to combine several product shapes that can have different needs:

  • personal data: bookmarks, mutes, drafts
  • gated content: paid newsletters, subscriber-only posts
  • socially shared data: private posts and stories
  • groups: private forums, communities, group chats

He also questioned the cost of creating a parallel data universe and called out URI-scheme complexity. This is worth tracking even if the thread did not fully engage Daniel's earlier long-form diaries. Richard has done foundational protocol/security work, so treat the critique as meaningful design pressure rather than as implementation guidance.

Eli Mallon pointed back to the diary motivation: for permissioned data, "rebroadcastability" is an antipattern. That is the core distinction that makes this more than ordinary public repo records with app-level filtering. If a reader can freely rebroadcast the same bytes into the public sync graph, the privacy boundary has already failed.

Daniel's reply clarified the current bet: the motivating use case is groups, and the proposal tries not to be dogmatic about one protocol per product category. If the group-shaped primitive is expressive enough for the simpler cases without becoming overwrought, that is a reasonable design win. He also said he was leaning back toward keeping at. Proposal 0016 now uses canonical at:// URIs with a literal space path marker, and ZDS follows that shape.

ZDS implications:

  • treat the at://{authority}/space/{type}/{skey} shape as the current experimental foundation, without maintaining the superseded scheme as an alias
  • keep private personal data as the motivating local use case, not a claim that ZDS has solved groups
  • do not encode a universal group model into ZDS while the proposal is still deliberately pushing group semantics up to applications/space hosts
  • preserve the ability to store private records and blobs now, but keep the protocol surface clearly experimental

broad matches #

The current ZDS implementation broadly matches these proposal instincts:

  • permissioned data is separate from public repo data
  • permissioned records use an at:// address with a space marker that names space authority, space type, space key, writer repo, collection, and record key
  • one writer has one permissioned repo per space
  • record sets are represented with LtHash-style set commitment state
  • general reader/group semantics should not be encoded as a universal protocol member list
  • apps that receive access to a space should expect whole-space read semantics, not just access to the authorizing user's own writer repo

proposal alignment points #

These are the main design points ZDS tracks against proposal 0016 and its follow-up pull requests:

  • Space declarations are lexicon definitions, not records. That appears to make a space type behave like a collection/schema authority: one NSID-shaped path declares a reusable space type that many space repos, accounts, spaces, and apps can share. This trades away the direct repo-authority feel of records in favor of namespace authority, fallback behavior, and a single path segment. App designers or project maintainers may publish the initial declaration using DNS and lexicon-publishing authority, but the declaration becomes a reusable commons resource after that. Lexicon unpublishing also should not break existing spaces that depend on the declaration, which makes lexicon resolution override behavior appropriate in a way that would be surprising for normal AT records.
  • ZDS should use the proposal names directly: getDelegationToken for the PDS-issued OAuth query, and listRepoOps for incremental permissioned-repo sync.
  • com.atproto.space.listRepos is implemented as a space-credential-only view of the authority's writer registry. notifyWrite advances that registry with the writer's revision and SHA-256 digest of its LtHash state; raw LtHash state remains private to the writer's repo host.
  • registerNotify stores an expiring whole-space registration for a service DID, and unregisterNotify removes it idempotently. ZDS follows the branch's 24-hour registration window.
  • Delegation tokens identify a requesting user and contain no app identity. Optional app identity arrives separately as a verified client attestation. Credential issuance and use follow PR #99's ES256 DPoP binding: the credential carries cnf.jkt and an attested client_id when present.
  • ZDS currently uses the proposal's permitted #atproto signing-key fallback. For remote authorities it resolves #atproto_space_host when present and falls back to #atproto_pds; ZDS does not yet publish dedicated space key or host entries for its own resident authorities.
  • Commit state uses versioned deniable commits: random ikm, a signature over (space, author, rev, ikm), and PR #100's direct HKDF-Expand-derived HMAC over the record-set hash. The CAR record index uses canonical DAG-CBOR map ordering. Wire bytes use AT JSON $bytes values.
  • OAuth space: scopes distinguish read from collection-constrained read_self, use authority=, and separate lifecycle/admin capability with repeated manage= parameters. Permission sets use spaceType, may name cross-namespace collections, and resolve authority=self at grant issuance.
  • Delegation tokens and verified client attestations are short-lived, single-use inputs consumed atomically when minting a space credential.
  • listBlobs enumerates the permissioned repo's referenced blob CIDs for full or revision-filtered sync; getBlob remains the authenticated byte path.
  • Space deletion semantics need review. The draft distinguishes space-authority deletion from member repo ownership and does not imply arbitrary erasure of every writer's local data.

implementation traps #

These are low-level details to keep visible before attempting parity work:

  • LtHash live-record elements must stay unique. The draft shape uses {collection}/{rkey}/{record_cid} so the same live element is not added more than once.
  • Byte order matters. LtHash lanes are little-endian, while the TLS-style length-prefixed context used around signed commit material is big-endian.
  • The operation log is a sync shortcut, not permanent history. A host may compact or drop it; syncers compare set hashes and recover from the full two-root CAR returned by getRepo.
  • Permissioned sync is relay-less and pull-based. Real-time behavior comes from write notifications through the space authority, but correctness cannot rely on every notification arriving.
  • Space types should be particular. Avoid designing one universal community space that gives every app access to every modality; apps can group many narrower spaces under a community authority at the application layer.

export and backup pressure #

The current proposal defines com.atproto.space.getRepo for full-state recovery. This is not the public MST repo shape: the CAR roots are a deniable signed commit and a DAG-CBOR map from collection/rkey to record CID. Record blocks follow in the same lexicographic order, while blobs are fetched separately. Account-wide backup still has to enumerate the resident's spaces and export each writer repo; the proposal does not define one aggregate backup archive.

For ZDS, avoid inventing a getSpaceRepo endpoint unless the proposal moves that way. The safer design pressure is exportability:

  • residents should be able to enumerate their own permissioned spaces
  • residents should be able to enumerate their own writer repos and records inside those spaces
  • backup/export can be a higher-level artifact, such as a manifest plus records grouped by space
  • blobs should probably be referenced or streamed separately from record export instead of assuming one giant blob archive is always practical
  • any future export path should be resumable or easy to retry for large media sets

This is separate from sync. Sync methods need protocol parity; backup/export can be an operator or resident affordance built from stable lower-level primitives.

simplespace surprise #

PR #94 removes the protocol-level member list as a universal authorization substrate, but it also sketches a required PDS baseline called com.atproto.simplespace.*.

That baseline currently includes:

  • createSpace
  • updateSpace
  • deleteSpace
  • addMember
  • removeMember
  • listMembers

It also introduces simple policies such as public, member-list, and managing-app, plus a managing-app access check hook.

This is not the same as reviving the older generic member-list experiment. ZDS now treats it as a small baseline space management API while still leaving richer application/group semantics to apps.

Some commentary describes the boring access model as a list of DIDs. In ZDS terms, do not read that as permission to restore the old protocol member-list sync surface. Treat it as space-management policy, especially for com.atproto.simplespace.*, including the service-authenticated checkUserAccess policy hook used during managing-app credential minting.

Implementation note: ZDS exposes the proposal namespace directly and does not carry the old com.atproto.space.createSpace / updateSpaceConfig / deleteSpace lifecycle surface. HappyView keeps compatibility aliases for its own older namespace; ZDS intentionally does not.

useful tests before more surface #

Before adding more endpoints, prefer tests that exercise durable semantics:

  • per-space and per-writer repo isolation
  • canonical permissioned at:// parsing, formatting, and stored-row migration
  • OAuth scope gating for authority reads, self reads, writes, and management
  • ranged blob reads through permissioned-data auth
  • write oplog ordering and cursor behavior
  • record-set commitment changes across create, update, and delete
  • deletion behavior for authority spaces versus writer repos

These tests should stay separate from public-repo conformance tests so the experimental surface can move without muddying stable PDS behavior.