Reading Proposal 0016: What atproto’s “Permissioned Data” Actually Does

AT Protocol is public by default. Everything you write goes into a signed, archived, and rebroadcastable repository that anyone can read from and moves through relays. This core design decision makes the network so legible and easy to build on, but it also blocks entire types of apps, like private groups, gated content, member-only forums, or anything that isn’t meant for everyone.

Proposal 0016 , Daniel Holmgren’s first draft of the “permissioned data” spec, moves us all one step toward solving this problem. It follows an eight-part dev diary series published this spring, and it has already inspired a community governance project called Arbiter, even before the draft is finalized.

I’ve been working toward this in my own projects, so I took a close look at the draft. Here are the ten points I think are most important, with sources. One note before we begin: this is an early draft , and as it says, “details, terminology, and behaviors are all likely to change,” so consider this a snapshot of something still evolving.

1. Permissioned data is a separate protocol

A key point up front in the document is that this does not change the existing public AT Protocol behavior, and it isn’t just an “addon”. It “runs alongside the public protocol” with “its own repository format, sync mechanism, addressing scheme, and resolution path.” AT Protocol is for open broadcasts: signed, archived, and rebroadcastable. The permissioned data spaces protocol enables data sharing between specific parties within defined access limits.

If you plan to implement this, keep in mind from the start: you’re building a separate data system, not just adding flags to the current one. The repo format, commits, sync process, and URIs are all different. The general idea is similar to public atproto, but most of the technical details are new.

2. It’s just access control

This choice has surprised people, and it’s the first thing the diary series addresses in “ To Encrypt or Not to Encrypt .” Permissioned data is not end-to-end encrypted. Authorized services, like your PDS and any apps you allow, can read it.

The reasoning is practical. Most permissioned systems focus on access control, not secrecy; for example, people don’t expect a private subreddit to use end-to-end encryption. Apps also need to read the data to work properly with features like search, notifications, trending topics, and moderation, all of which need backend access. End-to-end encryption also makes key management much harder for every client, and group encryption doesn’t scale well. For example, the MLS architecture in RFC 9750 aims for groups up to 50,000, but real-world use is much lower (Wire’s GA was tested with about 2,000 members), while these spaces are meant to support hundreds of thousands or even millions.

End-to-end encryption isn’t banned; it’s just an optional layer that apps can add if they want. Direct messages, which really do need end-to-end encryption, are not covered here. The spec is clear: “this protocol provides access control, not confidentiality.” So, don’t treat permissioned data as hidden from the server; it’s only private from users who aren’t authorized.

3. The “space” is the new load-bearing primitive

A space is an authorization and sync boundary representing a shared social context. It’s identified by a triple: an authority (a DID, the root of trust), a type (an NSID naming the modality), and an skey (a string disambiguating spaces of the same type under one authority).

One really important note (which took several diary posts to explain) is that a space isn’t a physical container. It “does not colocate records.” Each member keeps their own part of a space in a permissioned repo on their own PDS, and the space as a whole is “the union of these per-user repos across the network,” combined by the app. If you know how a Bluesky thread is built from posts on different PDSes, you already get the idea. A space can be as small as one person’s bookmarks or as large as a community of millions.

4. They replaced the MST with a quantum-secure set hash

Public AT Protocol uses a Merkle Search Tree to commit a repo. Permissioned repos work differently. Each one is summarized by a commit whose digest is a set hash over the current records. This means the order doesn’t matter; two repos with the same records will always have the same digest.

The construction is LtHash, the lattice-based homomorphic hash Meta open-sourced in Folly in 2019 (originally Bellare–Micciancio, EUROCRYPT 1997). State is a fixed 2048-byte buffer of 1024 little-endian uint16 lanes; each record is expanded with BLAKE3 in XOF mode and added or subtracted lane-wise mod 2¹⁶. Adds and removes are O(1), so there is no whole-repo recompute.

It’s helpful to know the background here. Until recently, the commit used ECMH (Elliptic Curve Multiset Hash), which is also incremental and O(1), but relies on elliptic-curve discrete log, so it isn’t quantum-safe. Later, Daniel said that “the quantum computing news is making me reconsider” the sync design. LtHash is based on the lattice Short Integer Solution problem, which is thought to be quantum-resistant, so it was chosen, even though it needs a much larger state. The trade-off, like with the MST it replaces, is that LtHash doesn’t support partial sync or single-record inclusion proofs.

One thing to watch out for: LtHash can have multiset collisions if you add the same input more than once. The spec avoids this by encoding each element as {collection}/{rkey}/{record_cid}, which is unique for each live record. Make sure you keep that uniqueness.

5. Commit signatures are intentionally deniable

I’ve come around on this position, and it is now my favorite part of the cryptographic design in the draft. A user doesn’t sign the content digest of a permissioned commit, because a signature over private content would let others prove what you wrote in a private space. That’s exactly what you don’t want for confidential data.

Instead, the signature covers only random bytes for each commit and some context, while the actual content digest is computed via a symmetric HMAC with a unique key for each reader. This way, readers get authenticity and integrity, but if a commit leaks, it’s deniable and doesn’t prove anything about its contents to outsiders. Each reader gets their own key. This approach was already described in “ The Big Picture ,” where Daniel called asymmetric signatures on permissioned data “an anti-pattern.”

If you’re implementing this, watch out for byte order: LtHash lanes use little-endian, but the TLS-style length-prefixed context uses big-endian. If you mix them up, cross-implementation verification will fail without warning.

6. Sync is relay-less and pull-based

There’s no main feed for permissioned data, and there can’t be, since these repos aren’t meant to be rebroadcast. Instead, a syncer keeps its own set hash and pulls updates from each member’s PDS using a listRepoOps call with a cursor for the operation log.

The smart part is that the system doesn’t need to receive every single operation to stay correct. The operation log is just a transport shortcut and not a permanent record. Hosts can shrink it or even drop it. Since syncing is based on comparing set hashes, any missed operation is caught at the next sync, and if things get out of sync, a full recovery is triggered. Real-time updates come from write notifications sent through the space authority, which listens to every repo in the space and forwards updates to registered syncers. Only accounts that have written are listed; readers are not.

7. Space authorization is a deliberately boring list of DIDs

“ Boring Auth ” sums up the access-control approach: a space’s members are just a simple list of DIDs. The draft goes further than before, removing write access from the protocol itself. Writes are enforced by readers, just as the Bluesky app already handles threadgates and blocks, rather than the protocol doing so.

This change came directly from community feedback. Meri pointed out that the old (DID, read|write) pair was “really just informational metadata” since the protocol didn’t enforce writes anyway, and Daniel agreed, so now the ACL is “literally just a list of DIDs.” Another detail: member lists are private and can be accessed only by those with access to the space.

It’s also worth noting what was left out: UCAN, even though Daniel helped create it. The reasons are that capability-based auth is “an acquired taste,” revocation would have to reach every writer in a distributed space, users want a member list they can see, and atproto is designed to be stateful and data-driven rather than based on calls.

8. “Apps get the whole space”

When a user grants an app access to a space, the app can sync every member’s repo in that space, not just the part belonging to the user who authorized it. This might seem like a lot, but the alternative is even more limiting.

The diaries looked at per-app, per-modality authorization (the “realms” idea) but dropped it. Per-app boundaries end up excluding smaller or experimental clients and make things more centralized, since the first app you authorize basically controls your data for that type. Allowing whole-space reads keeps things open. If a community doesn’t like this, they can switch to default-deny and only allow certain apps, and the allowlist is checked against the app’s cryptographically verified client ID, not just what it claims.

9. Every PDS must ship com.atproto.simplespace

The protocol doesn’t specify how spaces are created or who can read them. That’s left to a “space-management implementation.” However, every PDS must support at least one standard: com.atproto.simplespace, which is based on the user’s own DID and managed by a clear member list.

Its setup has three main options. mintPolicy (public, member-list, or managing-app) decides who can get credentials. appAccess (open-by-default or allowlist) decides which apps can access the space. managingApp points to an outside service. The managing-app mode is the most flexible: when credentials are created, the authority checks user access with an external app, so things like follower-gating, paid subscriptions, and join approvals all work without the protocol needing to know what a “follow” or “payment” is. simplespace is the required default, but it’s not the only allowed or special option.

10. Spaces should be particular

Daniel argues against “universal spaces” that treat all a community’s features as one big container. There are two main problems: it’s confusing on the OAuth consent screen (since the type is the consent string, like “access to your AtmoBoards forums”), and the all-or-nothing access boundary is not right, because an events app would also get your chat and photos. Instead, a community should be composed of many types of spaces (forums, events, chat, photos) grouped under a single community DID in the app layer. Communities can even have handles, just like users.

That intentionally open governance layer is where the proposal from Zicklag and Meri fits in. Arbiter is an RBAC service that works with spaces: roles, groups, and spaces are all the same thing. It creates a dedicated community DID (with a downloadable rotation key so you can leave a bad arbiter), and its main feature is space-to-space delegation. One space can be a member of another, allowing federated channels across communities while each side keeps control. It doesn’t require any changes to Proposal 0016; it connects directly to the managingApp hook and member-list system.

Where this is headed

This is just a first draft, and some tough problems are still unsolved: transferring ownership for spaces created under a personal DID, the “controlled DID” system a PDS needs to create and hand off community DIDs, and a cross-modality “by-authority-triggered-by-type” scope. Most of the real design debate is happening on Bluesky and in the community forum, not in the PR itself.

But the direction is clear, and it looks good. There’s a second data plane that keeps atproto’s flexible, app-driven style; a quantum-secure, deniable commit design that takes private-data threats seriously; and an authorization model simple enough to actually use. After years of hearing “atproto can’t do private,” this is a big step forward.