Weâre back!
Iâve been staying busy working on the implementation, brainstorming how to model communities, and daydreaming about drinking Modelos in the Chao Garden.
If this is the first diary youâre reading, I recommend starting with the Big Picture post as a good entryway point.
The main thing you need to know is: spaces are a new protocol primitive that define a shared social context with an access and sync perimeter, which allows applications to create data records that are not fully public.
That big picture post sketched out the authorization story at a high level. This post is going to dive a bit deeper into it.
Permissioned data is simple, actually*
*if you zoom all the way out & ignore all the details
I sometimes like to say atproto is composed of just three things:
an identity (DIDs)
a format for that identity to publish things (Repositories)
a schema language for describing those things (Lexicons)
Of course, anyone whoâs tried explaining the protocol knows itâs not actually that simple. And even my attempts at simplifying it down end up being 30 minutes long and make me feel a bit like the Charlie Kelly conspiracy meme.
But when you zoom out, I really do think atproto has a sort of obvious and abstractly simple shape to it.
Permissioned data adds one new primitive to that shape: a âspaceâ, which is just a boundary around some data and a list of accounts that can access it.
The protocol commits to one thing: if youâre on the list then you can read from the space.
Now, six diaries in, youâd be forgiven for thinking that this probably isnât actually that simple. And the truth is authorization is hard. Itâs the source of holy wars over how to do it right, and the worst kinds of security incidents when you do it wrong. Itâs the type of complexity that doesnât just get wrangled by devs & dev tooling alone. Auth necessarily pokes through into the application, because users have to understand who is getting to do what with whose data.
Authorization gets more difficult as you try to cover more use cases. Personal stuff like mutes and bookmarks is very straightforward, just you and maybe an app. A small private group may just be a list of members. A community needs admins, moderators, and roles. A paid newsletter wants tier-gated access. A private microblogging account gates access based on follows.
Each of these is probably fine on its own. The trouble is supporting the entire spectrum at the protocol layer. An extreme version runs the risk of becoming âAWS IAM on atprotoâ (a thought that should send shivers down your spine). But even without that level of complexity, weâd still be convoluting every implementation and paying the complexity tax for much simpler use cases, like mutes/bookmarks.
And thereâs only so much youâd want to push into the protocol. Policies that deal with application logic donât belong there. For instance, âonly people that follow the author can view this.â Follows are application-layer data that the protocol canât and shouldnât know what they are. If you want to build a permission layer around follow relationships, you need application-layer logic that translates that state into the protocol.
The alternative is to go in the other direction and try to push our authorization scheme down into the simplest primitive we can. This keeps the shared layer, the protocol, simple. From there, we can let complexity emerge through applications that can better wrangle it.
The member list
So: a list of DIDs.
Each space has exactly one list. Itâs controlled by the space authority DID. Every entry is a DID. If your DID is on the list, you can read and sync everything in the space. Thatâs it!
We actually decided to take it even one step further than what I laid out in The Big Picture diary and say that write access isnât encoded in the protocol.
Anyone with access to a space can âclaimâ to write to the space, and the applications that present that space determine if that write is âlegitâ. This functions in the same way as replies to a Bluesky post. The indexing applications ensure that each reply doesnât violate any blocks or threadgates before displaying it.
So writes are enforced by readers. This might sound like a funny enforcement strategy for a protocol, but it isnât really even a protocol thing at all. The protocol in some sense just doesnât have an opinion about writes. Anyone can do one and the readerâs app then does what every atproto consumer already does. It decides what to surface to the user from the data that it has indexed. The member list, and any other application data that encodes writes, are just inputs into that decision
Complexity above & below
The member list is the narrow waist that ultimately governs access and sync. But just because access is defined at the protocol level in such a simple way does not mean that we canât build expressive governance structures or complex application authorization semantics.
I see this complexity emerging both above and below the protocol.
If this section interests you, I encourage you to check out my last post about modeling communities on permissioned data & to share your thoughts on the community forum.
Above the protocol
Above the protocol is the application. This is where the social nuance ends up. Applications can layer arbitrary social and business logic on top of the protocol.
This may take the form of records, published in the space, that define application semantics around who can do what. For instance, a space could require that a member get 3 vouches from other members before they can post. This policy is expressed by a record in the space, the vouches are expressed as records, and the indexing application uses all these as inputs into the decision around whether it shows a posting userâs post.
Applications may represent richer community structures by composing together multiple spaces. A forum for instance, may have a moderators space in addition to the main forum space. A Discord-like app may even wish to create a separate space per channel!
Below the protocol
Below the protocol is the space host. A space host can encode arbitrary governance logic for a space. For instance, it may allow multiple accounts to administer a given community DID. There may be administrative tiers with different abilities. A space host may even implement a voting system for major choices in the community.
At the end of the day, each space is governed by a DID, and that DID is controlled using some key material. For most use cases, I believe a space host with governance logic will suffice. However, in exceptional cases, community owners may wish to use a scheme like Shamir Secret Sharing to jointly hold key material.
A note on UCAN
I didnât want to exhaustively go through all the different options we considered on the way to this proposal. But I do want to specifically mention UCAN, partly because itâs a spiritually adjacent alternative, partly because itâs the alternative that people have asked about the most, and partly because Iâm a co-author on the spec. Actually if you go back and look at the earliest atproto code (which I donât recommend doing đ ), the demos were built around UCAN. I really like UCAN. But a few things kept pushing me away from it in the context of atproto.
Capability-based authorization is powerful, but itâs also a bit of an acquired taste and isnât familiar to many devs. Atproto already taxes newcomers with a bunch of novel concepts and new ways of thinking, and Iâm reluctant to add any extra unfamiliarity.
Revocation can get gnarly. Revocation normally lives next to the resource. When the resource is a space distributed across every memberâs PDS, revocation has to fan out to every writer in the space, not just the space owner. This is tractable, but not trivial.
From a user perspective, I think itâs nice to be able to authoritatively enumerate who has access to content that youâre posting. It gives something concrete to hold on to. This requires a materialized member list.
Atproto is a stateful, data-driven protocol. You crawl data and determine if itâs authentic by looking at who wrote the data and how they signed it. UCANs on the other hand, usually get âinvokedâ at a point in time. You could store a UCAN in the data layer next to the content. But they are much larger than a signature alone, and they expire and would need to be continually updated to stay up to date.
Applications, not users
Okay so each space is defined by a list of users that can access it. Simple enough!
Except, as most atproto devs are aware, users donât often do reads. Or more specifically, the userâs client isnât actually doing the read at read-time. Applications proactively index data from the network, at write-time, non-interactively with the reader.
To illustrate, letâs say Alice uses an app called AtmoBoards to browse her forums. Bob posts into the Protocol Nerds forum that Alice is also a member of while Alice is asleep. The AtmoBoards app doesnât want to wait til Alice wakes up before it syncs Bobâs post. It wants to sync it right away, so that it has everything indexed and ready to go when Alice wakes up and logs in.
Four parties have to coordinate to make this read happen.
The reader: Alice
The writer: Bobâs PDS
The space owner: the Protocol Nerds forum DID
The application: the AtmoBoards app, which is doing the syncing on Aliceâs behalf.
And keep in mind, these are actually different parties. None are under the same authority. In many cases, each party has no prior knowledge of or trust relationship with the other parties.
Space credentials
Our answer to this is a stateless token issued by the space owner called a âspace credentialâ.
A space credential is:
Short-lived (a couple of hours)
Scoped to a specific space
Bound to the OAuth client that requested it
Asymmetrically signed by the space owner, so any member PDS can verify it without coordinating with the space owner
Usable with any member PDS to sync that memberâs permissioned repo
An application gets a space credential by proving to the space owner that it is syncing space contents on a memberâs behalf.
Walking through the flow: once a user logs into an application and grants that application access to a space, the application has an OAuth credential that the userâs PDS will respect. The application starts with the OAuth credential, then trades it with the userâs PDS for a âmember grantâ. The member grant is a one-time-use authentication token (similar to a service auth token) that is bound to both the space in question as well as the client ID of the application. The application uses the member grant token to authenticate with the space owner and request a space credential on behalf of the user.
So the credential flow goes: OAuth credential (PDS grants to application with consent of user) -> member grant (PDS grants to application) -> space credential (space owner grants to application).
A given application may serve more than one user with read access to a space. If so, it can choose which OAuth credential it wishes to use to get a space credential. It may even race multiple credential flows against one another. When an application loses all its member OAuth sessions, then it can no longer renew its space credential and it naturally loses access to the space.
Apps get the whole space?
When Alice authorizes the AtmoBoards app to access her permissioned data for the Protocol Nerds forum, AtmoBoards doesnât just get Aliceâs slice. It gets the whole forum. Every post from every member. Bobâs posts, Carolâs posts, and the 500 other members who Alice has never spoken to and who may not even know she exists.
If your immediate reaction is âwait, is that ok?â, then good, youâre paying attention & you had the same reaction that we did as we were working on this.
The alternative is the realm model discussed in diary 2, where every member of a forum had to individually authorize every application that any member of the forum wanted to use. The model is fine for a couple dominant apps, but slowly suffocating for everyone else. The long tail of weird, niche, experimental clients get iced out because they canât ever get a critical mass of users to individually grant access.
The whole reason atproto exists is so that the data you generate doesnât get trapped in a single app. It remains interoperable, composable, and remixable. To me, itâs absolutely imperative that the protocol doesnât lose those properties.
That being said, not every space is the same. Different communities have different needs. Communities with safety, security, or privacy concerns may wish to restrict the applications that can access content in their spaces.
This is the reason that the client ID of the requesting application gets threaded through to the member grant and even the space credential. At the end of the day, itâs the space owners decision to issue space credentials, and a member PDSâs decision to respect them.
The existence of a client ID allows a space owner to configure a list of applications that are able to sync the contents of a space. I believe âdefault allowâ with a deny list of applications is the null hypothesis for most spaces. But communities that are uncomfortable with that default may wish to adopt a âdefault denyâ policy and allow-list a small number of applications, even going so far as to allow only one particular application to have access.
To be clear, âdefault allowâ does not allow arbitrary applications to read from a space. The requesting application still needs a valid member grant from an authorized user.
Public spaces
What if a space owner just gives a credential to anyone that asks?
Good point! I donât see why not!
Iâm pretty on board with the idea of âpublic permissioned spacesâ and you can see from the proposed Lexicons that weâre currently planning to support them!
So why use public spaces as opposed to public atproto?
Public atproto is really geared towards âpublic broadcastâ. Data is signed, redistributable, and archival. Itâs âon the recordâ. Many modalities benefit from that! Some donât, and it may make sense in some cases for even public data to be transmitted in a more party-to-party manner.
As well, the permissioned and public protocols (though abstractly similar), function pretty differently. If content is likely to be flipped back and forth between public and not, doing so from within the same data protocol is much more straightforward than migrating data between protocols.
Closing thoughts
Authorization is the hardest part of the permissioned data protocol. Itâs the place weâve spent the most time second guessing. And itâs the place where âjust adding one more conceptâ feels the most reasonable and ages the worst.
Every time we sat with a problem, we'd end up back at "the simplest thing that could possibly work, with a clean place for apps to do their own thing on top".
So, itâs boring. But protocols are supposed to be boring! Itâs the stuff that gets built on them thatâs fun and exciting.
And speaking of the fun & exciting things being built, if this article interests you I really recommend you check out our discussion on modeling communities on top of the protocol.
The next post will be about the sync protocol, or what an application actually does once it has a space credential.
As always, stay tuned & let me know your thoughts!