Permissioned Data Diary 6: Boring Auth

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!