GeistHaus
log in · sign up

Daniel's Leaflets

Part of leaflet.pub

stories
Permissioned Data Diary 5: What’s in a Name?
In this permissioned data diary, we dive deep into the URI structure for permissioned data on atproto and use it to motivate a bunch of the larger design.
Show full content

It’s been a minute! Last time I wrote, we were all gearing up for AtmosphereConf and I shared the Big Picture of where our heads are at on this whole permissioned data protocol thing. It was a hot topic of conversation at the conference. It was great getting some feedback & reactions and hearing what other folks were working on in the permissioned data space.

If you haven’t yet, I highly recommend checking out the previous entries in this series before reading this one, especially the Big Picture post. We’re going to start getting into the weeds now.

To quickly situate ourselves:

  • Permissioned data is any type of non-public data ranging from personal data to large groups

  • Permission spaces (or just “spaces”) are our fundamental new protocol primitive that define an access and sync perimeter for data

  • Spaces have an owner and are generally hosted on a PDS

  • Users have a separate permissioned repo per space that holds all of their records for that space. That permissioned repo is hosted on the user’s PDS, not the space owner’s

This entry is going to be about how we identify a permissioned record. In other words, the URI structure for permissioned data.

Early on in the design process, I joked that if we could figure out the URI structure, then the rest of the protocol would write itself. This isn’t exactly true, but the URI really does express a lot of the larger design. So in this post, I’ll start by introducing a permissioned data URI. Then I’ll step through the URI segment by segment and use it as a device to reconstruct the design decisions that shaped it.

So without further ado:

✨✨✨

ats://did:example:space_did/com.example.space.type/space_key/
did:example:author_did/com.example.collection/record_key

✨✨✨

The scheme

You’ll notice that the scheme is not at://. We’re not sure about what the scheme should be (taking suggestions!), but we’re pretty sure it’s not at:// at least.

 There are a few reasons for this decision:

  • The resolution mechanism is pretty different for permissioned vs public data. Since URI resolution is informed by the scheme, we think it makes sense that the scheme is different.

  • There are light security implications to the different URI formats. Permissioned data URIs should essentially never be viewed outside of the perimeter of their space. Bumping into one of these in the wild should look & feel different from a public URI.

  • There is now a working group for AT Protocol at the IETF. One part of the charter is describing a URI scheme for the protocol. As we’re not (yet!) specifying the permissioned data protocol at the IETF, we want to avoid mixing up URI semantics with the working group’s work.

At the end of the day, specifying a new protocol scheme is basically owning up to the fact that, while we may re-use many primitives and roles from the public data protocol, we are specifying a new data and sync protocol, not just an extension to the existing protocol. 

The Space DID

Fundamentally, a space needs a name or an identifier. Let’s consider a couple options.

A slug

Maybe we keep it simple, and when you create a space you give it a name - something like “protocol-nerds” or “alice-forum”. We run into a few problems out the gate here. The first is that a slug doesn’t tell you anything about the space: who owns it, where the membership list lives, or how to resolve it. Slugs only work inside of an owned namespace (like a handle on a traditional social media site) that can maintain that mapping. Similarly, since the namespace is unowned, there’s no party that can prevent naming conflicts. Either intentionally or unintentionally, two users in two different corners of the Atmosphere could both create the “protocol-nerds” space and end up stepping on one another’s data.

A cryptographic nonce

To avoid collisions, users could generate a random cryptographic nonce instead. Of course if the nonce ever “leaks containment”, we’re back to the same problem of intentional collisions. But regardless of collisions, the fundamental problems with slugs remain: a nonce without a centralized resolver gives us no information about where or how to resolve information about the space.

The user’s DID

So we need something universal and resolvable? Sounds like a DID. Fortunately users in the Atmosphere already have a DID, maybe we can just reuse that. We could say that any space that Alice creates is created under her DID. But what happens when Alice wants to transfer ownership of the community?

If you’ve been around the Atmosphere long enough, you probably recognize this problem from feeds on Bluesky. Feeds are published as records and thus permanently tied to the account that created them. As feeds become load-bearing in ways that the creator may not have anticipated, this can turn into a big pain. The feed creator can’t hand off the feed to another account without breaking every backlink in the network.

Separately, I’d love to fix this problem with feeds, and I think we mostly can, but that’s a topic for another post.

We don’t want to repeat that problem for spaces (and I expect this problem will be even more acute for spaces than for feeds). Communities change hands all the time and it’s a brittle foundation to bake ownership into the identifier of the community.

A Space DID

So it sounds like DIDs work pretty well, but we’re concerned about hinging off of the creator’s DID. What if spaces could have their own DID?

Keep in mind, I’m not necessarily saying spaces are accounts. They have different lifecycles and governance concerns from user accounts. But the identifier problem for spaces is analogous to the identifier problem for accounts.

A few immediate follow ups:

Isn’t that a lot of DIDs? Well yes, maybe (and actually I’d question that assumption, I think there are more users than groups, for example Reddit only has 2-4 million active subreddits while having >1 billion active users). But even if it is, I actually don’t think there’s a problem. DIDs are cheap, and there’s no shortage of them.

Does every space need its own DID? Nope! More on this in the space key section.

Do spaces always need a non-user DID? Again no. Many spaces can just use the user’s DID. The answer for any given space comes down to a simple question: could this space ever change hands? For personal stuff like mutes, bookmarks, private posts, newsletters you publish, etc, the answer is no. These things are yours and they follow you around; they’re never going to belong to someone else. For anything social, the answer is yes. Even 1:1 DMs could technically transfer (one person may delete their account & the other wants to keep the thread). A space needs its own DID any time two or more people are sharing a space and there’s a possible future where the space and the creator of the space are not the same entity.

Note: This does imply that we’ll need a lightweight “controlled DID” system on the PDS so user accounts can manage (and transfer!) space DIDs. For expediency’s sake, we want to keep that scoped as tightly as possible and resist the pull toward building a generic managed-account system. But again, that’s a topic for another post.

The Space Type

A space is a fairly abstract thing. It really just provides an access and sync perimeter. But what’s it for? What’s inside? How do you users understand it? We need a mechanism to make a space legible without having to actually sync the contents of the space. This is the function of the space type.

Basically, a space type grounds a space in a particular modality. Spaces aren’t generic containers. They’re particular kinds of things - a forum, a group chat, a photo album, a subscriber list. The type is what binds a space to a particular modality or app context. 

A space type is an NSID that in turn resolves to a Lexicon document. Think of it as being kinda like a “collection” for spaces mixed with a permission-set Lexicon (hopefully that analogy isn’t too fraught, I’ll explain).

This particularity is important because it means we can discuss spaces with users, specifically in OAuth consent screens. Without a type, we’re left saying “Do you want to give this app access to did:example:group/protocol-nerds, did:example:other/cat-pics, and did:example:yet-another/besties?” If these are all of the same type (say com.atmoboards.forum), we can instead ask for access to all of these and more with actually readable text like “Do you want to give this app access to your AtmoBoards forums?”

A short aside that I think helps illustrate this point about particularity: some folks have compared spaces to circles (like the Google+ feature). I understand the instinct, but I think it’s a bad analogy. With circles, you attach one or more circles to different pieces of content. In a multi-modality network like the Atmosphere, you would expect your circles to function across many different modalities/apps. Reusable, multi-modality ACLs have the potential to accumulate a lot of complexity (and I also think you end up running into the problems from attempt 2 in diary 2). Spaces try to reign in this complexity. A space is a particular container, tied to a particular modality, with content posted into a particular space.

Including the type in the URI also lets devs and machines know what they’re dealing with before resolution. This is the same reason that collection is in public URIs. Type information is very helpful to know ahead of resolution. It lets you make all sorts of decisions about how you want to resolve, route, and validate what’s on the other end of the URI without having to make a network-hop.

Returning to my earlier analogy: in the URI, the space type functions like a collection. It lets you know what kind of thing you’re working with and roughly what you’ll find on the other side of resolution. As an actual Lexicon document, it functions like a permission-set in that it informs OAuth consent screens and allows application developers to bundle multiple resources under a single human-readable description.

The Space Key

The space key (or “skey”) is essentially our answer to the second question at the end of the space DID section (“Does every space need its own DID?”). 

The skey is a short arbitrary string that allows you to hang multiple spaces off a single DID. Like rkeys (“record keys”), these may be human-readable slugs, TIDs, cryptographic identifiers, or static strings with special semantics (like "self").

The most obvious case where you want multiple spaces with the same DID is personal spaces (spaces where your DID is the space DID): bookmarks, mutes, private posts, etc. These are all published off of the user’s DID, but have different access boundaries (for both users and for apps) and therefore each require their own space.

The utility of multiple spaces owned by a single identity also extends to the community use case. It’s not uncommon for a community to have multiple channels, each with different read constraints. For instance, a members-only community, a moderator-only chat, and a sub-channel within the community for some subset of members.

Putting it together

So to address a given space we have a DID, an NSID (the type) and an skey. Sound familiar? This is actually really nicely analogous to how we address records in public atproto!

  • Space: did:example:space_did/com.example.space.type/skey

  • Record: did:example:author_did/com.example.collection/rkey

I don’t know about you, but that symmetry feels pretty good on the brain to me!

The rest of the URI

Author DID, collection & rkey.

I feel like I don’t have to explain these too much. I said we were trying to keep this similar to the existing protocol didn’t I?

The order of the URI

Okay we have our 6 URI components:

  • Space DID

  • Space Type

  • Skey

  • Author DID

  • Collection

  • Rkey

I think there are two natural ways to lay out the URI:

  • did:example:author_did/SPACE/com.example.collection/rkey

  • SPACE/did:example:author_did/com.example.collection/rkey

Where SPACE is: did:example:space_did/com.example.space.type/skey.

At first I felt pretty strongly that we should go with the first option. The user is the authority for their data, and at the core of atproto ethos is identity-based authority. When you want to resolve a piece of data from a space, you go to the author’s PDS.

However as I tangled with this more and started working on our draft implementation, it started to feel less and less natural. And eventually I started to feel like I was arguing for adding tomato to the fruit salad.

So I’ll make the philosophical, practical, intuitive, and aesthetic cases for why I now think we should go with the latter.

Philosophically the case for the latter is that the user only has the authority to post within a space’s boundary by virtue of the fact that they were included in the space’s member list by the space itself. Therefore, the author’s authority is downstream from the space. As well, atproto ethos is around “identity-based authority”. A space still has a DID (identity) as the authority and therefore still holds to the ethos.

Practically, the latter is much cleaner. As I started working on our sketch implementation, I found that references to spaces show up a lot. Being able to talk about those references as “partial space URIs” and reusing the tooling we have for space URIs is very nice. If space reference is internal to the URI (as it is in the first example), then we can’t treat it as a partial URI - it’s some other thing.

Intuitively, I think the latter just feels more natural to use and reason about. When working with the protocol, you have a sense that a record is “in a space”. Which implies that the space needs to come before any of the record details - including the author.

Aesthetically, I find the second to be much nicer as it preserves the symmetry between the space part of the URI & the record part of the URI. It goes DID-NSID-string-DID-NSID-string, rather than DID-DID-NSID-string-NSID-string.

URI golf

But these URIs are so long! 

Yeah you’re right. Six segments is a lot. It kinda sucks, and it’s kinda ugly. But unfortunately I think we need them all. 

We considered tricks like using relative URIs from within a space or collapsing the DIDs if the space DID & author DID are the same.

The rub with any of these tricks is that you now have two URIs for the same resource and you lose string equality which is the most valuable property of a URI. Once you lose that, you have to run a canonicalize function every time you want to compare URIs. It’s just not worth whatever byte savings you’re going to get.

(Yes, I know that public at-uris can have DIDs or handles as authority and therefore don’t have strict string equality. I regret this & encourage everyone to only use DID-based URIs.)

This makes me think of something that @jacob.gold used to say when @divy.zone or I started worrying about some sort of pre-optimization of some system: “I’ll pay for it”. Basically, it was his way of pushing back and saying “whatever savings you’re hoping to get out of this aren’t of the order of magnitude that would make it a worthwhile thing to pour energy into”. That’s basically how I feel about shortening the URIs.

Computers are fast, bandwidth is cheap, and all of this is going to be overshadowed by quantum-resistant signatures soon anyway. The savings aren’t worth it. I’ll pay for it.

Closing thoughts

As always, please let me know your thoughts!

These diaries will be coming a bit slower than they did before AtmosphereConf. I have some very rough sketches of an implementation on a public branch in the atproto repo. Please don’t over-index on it! I’ll continue to give updates as I explore.

The next post will either be about access control or the sync protocol, I haven’t decided yet. Probably whichever is feeling sturdier at the time. I thought sync was basically settled but the quantum computing news is making me reconsider a bit.

Anyways, stay tuned!

https://dholms.leaflet.pub/3mlegohgtps2k
Permissioned Data Diary 4: The Big Picture
A special edition of the data diary that sketches out the rough shape of where we're heading.
Show full content

Over the last three posts (four including the rename) we’ve been narrowing in on a design for permissioned data in atproto. At this point we’ve established the general shape of the network architecture: permission spaces as network-wide access and sync boundaries that establish a shared context. In each post, I weighed a few different options, made arguments for each, and then advocated for the direction where we’re taking the design. 

This post is a little bit different. I’m going to zoom out and give a rough sketch of the full picture. This post won’t be diving into tradeoffs or making the arguments for our choices - those will follow in later posts. I’ll also try to avoid getting bogged down in exact details or mechanisms except where necessary. This post is also lower confidence than previous posts and subject to change (especially as we get feedback from you all!). 

I’m writing this for a couple reasons. First, some of the design decisions from here are more intertwined than the big picture ones that the previous posts dealt with. Access control isn’t completely separate from sync. It’s useful to have a high-level signpost of the full picture as we dive into each individual design choice. Second, AtmosphereConf is coming up! If you remember from my first post, I said I had “a rough goal of having a rough proposal by AtmosphereConf“. This is that rough proposal! I know folks will be eager to talk about this at the conference, so I wanted to make sure I got the big picture out beforehand.

Alrighty let’s get into it!

Permission Space

A permission space (or space for short) is an authorization and sync boundary for permissioned records representing a shared social context in the network. It can include many different types of records from many users. Each user stores their own records for a given space on their own PDS. The space exists not as a physical container but as a coordination concept: a shared identity, access control list, and sync boundary. Spaces can scale all the way down to a user’s personal data such as bookmarks/mutes or all the way up to a million(s) person forum or group.

Space Owner

Each space is associated with a space owner: a DID that serves as the authority or root of trust for that space. Space owners can establish many spaces. The space owner’s DID document contains at minimum a signing key and a service endpoint for the space.

For personal data, the space owner is simply the user's own DID. For shared spaces like communities, a dedicated space DID is preferable. A dedicated DID allows ownership to be transferred by updating the DID document without breaking any existing references into the space.

By default, users will be able to host a space on their PDS and the reference implementation for spaces will ship in the PDS. However the notion of a space and the interface for services that host a space is defined at the protocol level and not inherently linked to the PDS. Any service may create and host a space if it supports the required APIs.

This likely means that we will need a “controlled DID” system on the PDS so that user accounts can manage community space DIDs. The intent would be to keep this system as lightweight as possible and avoid a generic controlled account system.

Space Type

Each space has an NSID that describes the “type” or “modality” of the space - what kind of data it contains and how it's used. This type is defined by a published lexicon and serves as the OAuth consent boundary. When a user logs into an application, they grant access based on the type of space. For example: app.bsky.group, com.atmoboards.forum, app.bsky.personal.

Permissioned Repo

For each space that a user participates in, they have a permissioned repo. Each user is expected to have many permissioned repos. A permissioned repo exposes a CRUD interface that is similar to the public repo and may contain many different types of records. A space is composed of anywhere from 1 to millions of permissioned repos.

Record addressing

A space is addressed by the space owner, the type, and a key. The space key (skey) is similar to a record key (rkey); it’s an arbitrary string that differentiates multiple spaces of the same type under the same owner.

To address a permissioned record, six pieces of information are needed:

  • Space owner (DID)

  • Space type (NSID)

  • Space key (string)

  • User (DID)

  • Collection (NSID)

  • Record key (string)

We’re currently arguing about what should be considered the authority of the URI. The user DID because the record is hosted by the user and authority for the record ultimately rests with the user? Or the space owner DID because it authorizes a user’s ability to create a record in the space in the first place?

We haven't settled on a scheme yet. It probably won't be at:// — permissioned data URIs resolve through a completely different protocol from public data, and as such, we feel they should be visually distinct. ats:// is a likely candidate.

Access controlMember list

Each space has a single member list. Each entry is a (DID, read|write) tuple. Write access is inclusive of read access. This is the only ACL data structure.

Write enforcement is ultimately handled by readers of the space. Any user can purport to write to a space on their PDS, but readers of the space compare incoming records against the member list and use that to define the real content perimeter.

The member list is published by the space owner and is synced via the same sync protocol as permissioned repos.

Space credentials

To read records from a space, a reader needs a space credential. A space credential is a stateless authorization token issued by the space owner. 

They are: 

  • Short-lived (~2-4 hour expiration)

  • Scoped to a specific space

  • Asymmetrically signed by the space owner's key and thus verifiable without coordinating with the owner

  • Usable with any member PDS to read space contents

Space credentials are how applications gain access to sync repos within a space. A service that wants to sync space contents does the following: 

  • uses an OAuth credential from a member user to obtain a member grant token (similar to a service auth token but bound to client ID) from that user's PDS

  • presents that grant token to the space owner in exchange for a space credential

  • uses the space credential to sync the member list from the space owner and sync records from each member's PDS

When a service loses all its member OAuth sessions (because users left or revoked access) it can no longer renew its credential and naturally loses access to the space.

Application allow/deny lists

Spaces can be configured as "default allow" or "default deny" for service access. In default-deny mode, an explicit allowlist of application client IDs can be specified. In default-allow mode, a denylist. This is internal space configuration and is not visible to members, though applications may choose to publish it in a record if they want to.

Default allow is considered the natural choice for spaces. It supports atmospheric interoperation (internection? 😏) between applications. Default deny is offered for social modalities that are more context specific, scoped to a particular ecosystem/community, or have more rigorous security needs.

This configuration would be handled by the app that creates the space, in tandem with the user. They are in the best position to understand the security/privacy implications of interoperation with third-party clients.

Sync

Sync is pull-based. Applications are responsible for staying in sync with all member PDSes. PDSes assist by sending lightweight write notifications to prompt pulls when new data is written.

Permissioned repo commits

Each user’s permissioned repo within a space is represented by a cryptographic commit: a compact digest that characterizes the current set of live records, independent of history. 

We use ECMH (Elliptic Curve Multiset Hash), a set hash where adding or removing an element is a single point operation rather than a full recompute of the hash. Two permissioned repos with the same live records produce the same digest regardless of the history of operations.

This commitment plays the same role as the MST root hash for public data: a single value that definitively characterizes what's in the repo. Unlike the MST, it does not support partial sync or single record proofs. The tradeoff is a noticeably lower overhead cryptographic structure and sync protocol.

The ECMH for a permissioned repo is authenticated using a randomly generated and transient HMAC key, which is in turn signed by the user’s atproto signing key. 

The commit is composed of the ECMH hash, the computed HMAC, the HMAC key, and the signature.

Each commit is authenticated for only one party and is not intended to be rebroadcast. HMACs are used to provide deniability in the event a signature is exposed. Every reader who reads a permissioned repo from a PDS will receive a different HMAC key.

Sync log

Sync operates via a log of recent write operations. Services query with a since parameter to retrieve operations for a given permissioned repo since their last known position. The oplog is a transport optimization, not a committed data structure. The repo host may compact or flush it at any time, though it's expected to keep it available for a backfill window.

If a consumer falls behind, the oplog is unavailable, or the commitment doesn't match after replaying operations, the consumer can always fall back to syncing the full repo state. Given expected repo sizes, a full resync is not prohibitively expensive.

Write notifications

Real-time sync is achieved by sending write notifications to syncing services. Member PDSes don't necessarily know the full list of sync services, so notifications are routed through the space owner. Each service then initiates a pull from the user's PDS. Write notifications are best-effort. If a notification is dropped, sync is ultimately self-healing through the permissioned repo commit mechanism.

App coordination

Spaces are most often hosted on PDSes, but the logic for governing a space typically lives in an application. Therefore space hosts expose a set of APIs that applications call, using the space owner's OAuth credential, to manage members, transfer ownership, and update configuration.

In some cases, ACLs may be governed by some application-level concept that isn’t expressible in the protocol: join requests, approval flows, invite links, etc. A user on another application may not know which application it needs to talk to in order to initiate one of these flows. The only service they know of is the host of the space itself.

To address this, each space can be configured with a managing app that it will generically route application-level requests to. This is purely internal config and is not visible to members.

Closing thoughts

That’s the big picture, at least as it stands today! As I mentioned in the intro, this is lower confidence than the previous posts. We’re still working through things and most of the little details still need to be hashed out. Hopefully this answers more questions than it inspires. For the questions that it does raise, we’ve hopefully thought of them as well and at least have intimations of an answer!

I’ll get into more detail on the alternatives we considered and the tradeoffs in later blog posts. In the meantime, let me know your thoughts/feedback/questions! And looking forward to seeing folks at AtmosphereConf next week!

https://dholms.leaflet.pub/3mhj6bcqats2o
Permissioned Data Interlude: Spaces
In which I retcon the naming of everything.
Show full content

If you’ve been following along on my series of blog posts about permissioned data, then you’re familiar with the concept of a bucket. This is the new protocol primitive that represents the authorization and sync boundary for a shared context.

I used the word “bucket” 41 times in the last post, and am not legally liable for any hospitalizations that resulted from that:

Many people pointed out that “bucket” was a weird word for the concept we’re trying to communicate and doesn’t give the proper intuition. It’s time for me to admit that you’re right, eat a bucket of crow, and retcon this whole thing with a more intuitive name.

This is how you know we’re really figuring it out as we go along!

The problems with bucket

The biggest problem with the word bucket is that it implies data locality. I spent the last post arguing against a colocated bucket. I think this intuition is harder to establish than it ought to be while using the term “bucket”. If we had used a different name for this shared context, it may have been easier (both internally & publicly) to get to an understanding of them as being partitioned or sharded over all the PDSes of the members.

My other problem with “bucket” is that it doesn’t give us a good word for a user’s partition of the bucket (the thing with the actual commit over it). What do we call these? Bucket partitions? Bucket shards? Bucket segments? Bucket repos? None of these quite have the right ring to them. I think this is because “bucket” already sounds like a container and normally the container is the thing with the commit on it.

Permission spaces

After grabbing a fresh bucket of paint and working on the bikeshed with the protocol crew, we’ve decided on the term “Permission Space” or “Space” for short.

The term for a particular member’s records in a given space is a “Permissioned Repo”. Instead of taking the perspective of the space (which is made up of a bunch of partitions or shares) we take the perspective of the user (who has a permissioned repo that is shared with a particular permission space).

We feel that “permission space” gives better intuition about how this primitive functions primarily as an authorization/access boundary. It doesn’t sound like a container and as a result doesn’t imply data locality. “Permission space” and “space” are technical enough and fit the vibe of other atproto words like “repo”, “collection”, and “record”. However “space” is also accessible to non-technical users who may encounter the term in their PDS account management dashboard or during an account migration. “Permissioned repo” builds on the intuition of “repo” from the public protocol and prevents us from having to introduce an additional term.

Next up

Okay had to get that out of the way! I’m still planning to put out another post this week that gives a rough high-level sketch of the big picture for permissioned data including addressing, access control, and sync. I just didn’t want to switch up the terminology at the same time that I’m trying to describe the whole system. That post will use this new terminology so you can see it in action.

Stay tuned!

https://dholms.leaflet.pub/3mhbuoc64xk2a
Permissioned Data Diary 3: Your Bucket, My Data
The third in a series of posts building up a solution to permissioned data on atproto. We look at two different models for where buckets live and why the simpler-looking one doesn’t work out.
Show full content

Welcome back to the permissioned data diary! Quick recap of where we’ve been: post one was about why we’re not doing E2EE. Post two was about why permissioned data needs a shared context with a perimeter, a new protocol level detail that we’re calling a “bucket”. 

I gave a teaser at the end of the last post:

a bucket doesn’t necessarily imply a physical container sitting on one PDS

This post is going to dive into exactly that: where the data in a bucket actually lives and where applications in the network fetch it from.

Some setup

Let’s carry on our example from last time. AtmoBoards is a forum app, and Alice wants to create a private community forum on there called “protocol nerds”. Alice creates a bucket to represent the forum and invites a few thousand other users including Bob and Carol.

There are two fundamentally different ways you could design how this bucket is stored and synced. For this post I’ll be calling them colocated and partitioned.

A colocated bucket lives on the owner’s PDS. All writes from all members get sent to Alice’s PDS. All reads go through her PDS, and all apps sync from her PDS. Everything flows through one place. In other words everything in the bucket gets “colocated” on Alice’s PDS. Bob and Carol might keep copies of their own data on their PDSes, but those are essentially just backups. The authoritative source of truth for bucket contents is Alice’s PDS.

A partitioned bucket on the other hand exists as an abstraction rather than being localized on a single server. It exists across all of its members’ PDSes. Alice’s PDS (being the owner) may still play a special role in administering the bucket. But Alice, Bob, and Carol each store their partition of data for the bucket on their own PDS. Apps that want to sync the bucket have to go talk to each member’s PDS and stitch it together. The intuition for this is to think of how a post thread in Bluesky works. The posts are not colocated at the protocol level; they are constructed into a “thread” by virtue of the fact that they all reference the same root post.

The wrong kind of simple

When you first think of a bucket, or even a group/forum for that matter: it feels like a thing that exists somewhere. The model of a colocated bucket leans into that intuition. It’s the first instinct & null hypothesis. 

Colocated buckets are also simpler technically. There’s one place to go to sync a bucket; you don’t have to run around to every single member’s PDS and sync each of their partitions of the bucket separately. We can have a single logical clock or cryptographic commitment on a bucket to help with sync rather than splintering that state over all members. There’s a single point of enforcement for access control. All reads and all writes go to the bucket owner who knows exactly who has access to the bucket. Without the bucket owner as authorization enforcer, this role gets diffused throughout all participating member PDSes and applications.

However this technical simplicity masks a deeper confusion and complexity around the roles in the network and their responsibilities. Specifically, the role of Alice’s PDS changes from being responsible for Alice’s data to being responsible for the entire bucket’s data. Hosting data on behalf of every person on her forum is a completely different kind of commitment.

This grates against our atproto ethos. But it isn’t just philosophically awkward, it cascades into some pretty gnarly real-world problems.

Resource usage

To start with, Alice’s PDS has to store everything for the forum! Every post from every member. For a small group of friends that’s fine. For a community of 100k people, Alice’s PDS is now running infrastructure and storing data at a scale that has nothing to do with Alice’s own usage. 

This problem is even worse if Alice is a self-hoster. Her PDS’s job just radically changed. She now has people she doesn’t know, posting content that she can’t control directly into her PDS. She didn’t sign up for this! She doesn’t want to be on the hook for thousands of users’ posts and likes and images. She just wanted to create a forum for her protocol nerd friends.

Moderation

In atproto, applications generally take on the responsibility for network-wide moderation. PDSes may do some moderation as well, but PDSes never need to moderate anything on the network not generated by an account hosted on that PDS. With colocated buckets, Alice’s PDS is suddenly on the hook for hosting and serving data for people that it has no relationship with - no account, no email or method of contact, and no Terms of Service.

Blobs/Media

As a special case of resource usage & moderation, consider blobs/media. If Alice’s PDS is hosting the bucket, it’s hosting Bob’s uploaded images too. Now Alice’s PDS has copyright exposure, CSAM risk, etc. Bob can upload giant files that affect Alice’s storage quote, not his. 

We could say colocated buckets are only for record data and blobs are always stored on the author’s PDS. However this gets us back to the same difficulties we encounter with partitioned buckets though perhaps at a lower magnitude.

Takedowns

When Bob gets taken down at the hosting layer, his PDS’s job right now is simple: stop serving his content.  For colocated buckets, it becomes: fan out a removal request to every bucket Bob participates in, have those PDSes honor it, and coordinate re-instatement if Bob migrates to a new PDS. Not only can that get pretty complicated, but it’s a pretty strange and awkward thing for Bob’s PDS to do. Its responsibility transitions from “deciding whether to provide a service to Bob” to “actively coordinating data removal across the network”.

Operational dependency

Colocated buckets require the introduction of an operational dependency on the owner’s PDS. When Bob posts, he has to send that post to Alice’s PDS. If Alice’s PDS is down, what happens? One option is that the post fails. Now Bob is experiencing failures because of a service that he has no real relationship with. The other option is that the post buffers on Bob’s PDS and is sent to Alice’s PDS when it comes back online. This now requires a more complicated PDS<>PDS sync mechanism (at the least, an outbox). Even in this case, the group will receive no updates until Alice’s PDS comes back online.

This is all difficult to communicate to users. A user has a relationship with their app and their PDS, but they have no real relationship with a bucket owner’s PDS, and now their experience of using their app is downstream of that service.

Trust 

Say Carol wants to read a post from Bob on the forum. In the colocated model, her app fetches it from Alice's PDS, a service she has no relationship with. Carol trusts her app to some degree, and she probably trusts Bob's PDS to accurately represent Bob's content. But Alice's PDS is a total stranger in this transaction. Colocated buckets introduce an extra “hop” on the way from the author’s PDS to the application, specifically a hop that a reader doesn’t necessarily trust.

The way we handle these untrusted hops in the public protocol is to asymmetrically sign data. However, I strongly consider asymmetric signatures on permissioned data to be an anti-pattern (about which I’ll have more to say in a later post). But even if we were to sign it, signing data and storing it on a wide variety of other PDSes has the downstream effect of dramatically increasing the complexity of key rotation and therefore account migration.

The right kind of complex

(does that header inspire confidence?)

In contrast, partitioned buckets are more complex technically but also more honest in how they divvy up responsibilities. They are much closer to our “atproto ethos”. Users own their data. It lives on their PDS. Applications crawl it.

The whole model of the atmosphere is that apps construct views by aggregating records from a fluid and universally addressed hosting layer made up of many PDSes.

Partitioned buckets are that same model, with access control layered on top. An app that wants to sync the Protocol Nerds forum goes and talks to Alice’s PDS, Bob’s PDS, Carol’s PDS, and all the others and stitches together the view. This is more work for the app. But it also mirrors how atproto works for public data. 

The tradeoff is the complexity. Apps need to maintain relationships with many PDSes. Sync state is harder to reason about. And access control starts to rear its ugly head. 

These are hard problems. But they are also engineering problems. They’re the kind of hard that a protocol can hopefully solve. To my eye, the problems with colocated buckets are architectural. They’re breakdowns in who’s responsible for what and violations of trust assumptions at the core of atproto. I don’t think they’re things we can engineer our way out of.

What's next?

What’s next is going to be about trying to wrangle this complexity! I have a few topics in mind:

  • Bucket & record addressing: How do we name and locate buckets? How do we resolve a permissioned data URI to a record?

  • Access control: How do buckets track who can read and write? How do apps prove they’re allowed to read?

  • Sync protocol: How does the data actually get from a PDS to the app? If we’re not signing it, what are we doing? If we’re not using an MST, what are we using?

Just on a personal note, I’ve been enjoying putting these out and hearing thoughts and feedback from everyone. Big thanks to everyone that’s been following along. If it seems like we don’t have all the answers yet, well… we don’t. I’m writing these more or less as we get confidence in decisions. But things are slowly starting to shape up and I think we’re starting to circle around a pretty complete picture for a permissioned data protocol. 

Next week (ahead of AtmosphereConf), I’ll put out a special edition of the permissioned data diary that includes a low-resolution sketch of that complete picture. It won’t be as in-depth as the previous posts or try to explain how we reached certain decisions. Instead, it’ll serve as a high-level signpost and should give us something to chat about in Vancouver.

Stay tuned!

https://dholms.leaflet.pub/3mguviy6iks2a
Permissioned Data Diary 2: Buckets
The second in a series of posts building up a solution to permissioned data on atproto. We introduce buckets: a new protocol primitive for creating a shared social context.
Show full content

In the last post, I discussed end-to-end encryption and why we’re focusing instead on designing a permissioned data system. That was a narrowing down of the problem space. In this post, I’m going to start building up an actual solution. I’ll be introducing a new concept that is core to how we’re thinking about permissioned data: buckets.

What we’re trying to do here

Before diving into solutions, let’s first ground ourselves in what we’re trying to achieve. 

Permissioned data should feel like a natural extension of how public data already works in atproto. That doesn’t mean that we need to re-use the exact data structures and sync protocols as the public data system. However, the general shape should be familiar: users publish records, those records are canonically stored in the user’s PDS, applications crawl PDSes and sync data to build their own views, users own their data and can move around, authority rests in the DID that publishes the data, the system works at scale, and users shouldn’t feel they’re dealing with weird behavior just because they’re on a decentralized protocol.

Ideally we handle permissioned data with one coherent protocol rather than a patchwork of different systems for different use cases, though we shouldn’t be dogmatic about that if the design demands otherwise.

Groups are the hard case

Let’s revisit the modalities that I called out last time (excluding messaging this time):

  • Personal Data: mutes, bookmarks, drafts

  • Gated content: Patreon, Substack, paid newsletters

  • Socially shared: private posts, stories

  • Groups: Facebook groups, private forums, private subreddits

These are roughly ordered by complexity. Personal data is simple, it’s just you, your PDS, and maybe an application or two acting on your behalf. Gated content is one-to-many with a clear gatekeeper. Social sharing introduces some dynamism around who is able to view your stuff, how they interact with it, and who can see their interactions.

Groups are many-to-many. They have dynamic membership - people join & leave, admins change, ownership of the group changes. Many users are contributing content to a shared context. Users in these groups may want to view their groups in any number of different apps.

My hunch is that if we can design a system that works for groups, the simpler modalities will fall out naturally. Groups force you to confront the hardest questions about ownership, membership, and access control. So that’s the modality I’ll focus on in this post.

I’ll sketch out two possible solutions and then introduce the concept of a “bucket” which resolves the issues from each of the preceding solutions.

Attempt 1: App-controlled access (realms)

Think about the role an application plays in deciding what a user is able to see. Even in public social modalities like Bluesky (as in apps built on the app.bsky.* lexicons), the application prevents users from seeing posts that violate thread gates and post gates. Blocks prevent the blocked user from viewing certain posts and also prevent third parties from viewing block-violating posts. Of course, for public content you can always go directly to the protocol. Still, this is a basic form of access control being applied by the application.

One clean solution to permissioned data may be to say “let applications handle it”. If an application has access to all permissioned data for some particular social modality, then it can apply arbitrarily complex access control rules around which users are able to see which content in which context. Applications are in the best position to do this because they fully understand the business logic behind who has access to what. Keep the protocol logic coarse and give applications full flexibility around access control logic.

For applications to fulfill this function, they need access to all the content for a particular modality. Let’s call this a “realm”. A realm is an abstract content partition in the network intended for a particular “type” or “use” of permissioned data. Realms can be identified by an NSID defined by publishing a lexicon. When a user creates a permissioned record, they specify the realm that it is being posted into.

To make this concrete, consider a private forum application called “AtmoBoards”. AtmoBoards can define a new realm by creating a lexicon with an NSID, something like com.atmoboards.forum. A realm is network-wide and heterogeneous. The AtmoBoards realm contains posts, comments, profiles, votes, and more. It contains all AtmoBoards forum content from all users.

Access to the realm naturally translates into an authorization scope that can be displayed on the OAuth consent screen. Something like “Your content within AtmoBoards forums”. The app then simply syncs the data from the user’s PDS using the given OAuth credential. Applications don’t intrinsically “get access to the whole realm”. However, when a user logs into an application that works with a particular realm, that application requests access to all of that user’s content in that realm.

This is conceptually pretty elegant. We get to simply reuse existing auth infrastructure. The consent flow is legible to users. Applications can offer arbitrarily complex access control rules on a user-by-user basis. Users maintain the canonical copy of their data which enables users to choose their application and therefore migrate their community between applications.

However, problems start to emerge when users in the same group use different applications.

Say Alice, Bob, and Carol are all in a private forum together. Alice and Bob use AtmoBoards, but Carol uses a new app called ForumBrowser. Bob posts something in the group. Ideally Carol should be able to see it because she’s in the group! But ForumBrowser can’t access Bob’s post unless Bob has separately authorized ForumBrowser through an OAuth flow. Bob probably hasn’t, he might not even know ForumBrowser exists! 

Maybe we introduce a programmatic way to give access to a given application without going through the OAuth flow. If Bob and Alice both decide that Carol should be able to access this forum through ForumBrowser, they could choose to grant it access to their content in the com.atmoboards.forum realm. However, remember this realm contains all content from all forums. So in granting ForumBrowser access to this particular forum, they also grant ForumBrowser access to all of their forum content from across all forums.

This highlights the basic problem with realms: the protocol-level access boundary is too coarse. You either give all of your AtmoBoards content to an app or not. If a group wants to support more than one application, every member of the group needs to give access to every application that the group supports. 

Some users may feel comfortable with this if there are just one or two big applications in the ecosystem. If someone in my forum wants to use a weird little niche app, I might even feel comfortable sharing that particular forum with them. But I don’t want to give every experimental app access to all of my private forum content.

This becomes a centralizing force. Applications aren’t that useful if they don’t have access to the full set of content for any given forum. Each application needs a critical mass of users to individually authorize each app. Big apps might eventually get there. But the long tail of apps - the experimental ones, the niche ones, the apps that make an open ecosystem interesting & engaging - are completely iced out. This grates against the core value proposition of building on an open protocol: the data should be interoperable, composable, and remixable across applications. 

This suggests that we need a more fine-grained mechanism to manage access. And we can’t just riff off of OAuth; our mechanism needs to be programmatically expressible at the protocol layer.

(That said, we might not have seen the last of realms. Keep an eye out for them in a later post 👀)

Attempt 2: Granular user-controlled access

The immediate followup question is: what’s the unit of access control? What is the ACL actually attached to? And how does that ACL get updated?

Let’s walk through a couple scenarios and watch as the complexity escalates.

Note: As we go through these scenarios, I’ll discuss in terms of user-to-user access grants. Applications need to sync on behalf of users which is a whole ‘nother problem that we’ll address in a later blogpost. However, the same basic logic applies to user-to-app access grants.

Simple case: Alice wants to share some permissioned posts with Bob. She puts an ACL on her posts collection granting Bob read access. Great!

A bit harder: Alice has different types of permissioned posts meant for different people - posts for close friends, posts for her mutuals, posts for a paid community. Now she needs separate ACLs for each category. We can’t express these at the collection level anymore, but maybe we can attach ACLs at the record level. Okay, more bookkeeping but still tractable.

Harder still: Alice shares a permissioned post with Bob & Carol, and Bob replies. Carol wants to read the thread. She now needs access to both Alice’s and Bob’s permissioned data. Who coordinates that? Alice and Bob each have their own ACLs on their own PDS. 

And the real kicker: Alice creates a private community group. She adds Bob and Carol. A bit later, she adds Dan and Eve. Over time, Alice adds fifty more people. Every time someone new joins, every existing member’s ACL on every piece of content in the group needs to be updated to reflect that the new member can now see their contributions to the group. Carol may not even know Dan or Eve much less the 50 other members. Is she individually responsible for updating her ACL when the group owner adds someone? How does she get notified? Even if we invent some sub-protocol for managing ACL updates, can we really expect all group members to update the ACL on every single record meant for the group each time the group boundary changes?

The fundamental issue here is that when access control is attached to individual pieces of data, every social interaction creates a coordination problem. As the number of participants increases, that coordination overhead starts to exponentially spin out of control.

Social interaction revolves around a shared context. What we really want is a way to say: “Here’s a space. Here’s who has access to it. Everything in this space inherits that access”.

We need a bucket. 

Buckets

A bucket is a named container that holds records and has a single authoritative ACL. It’s the protocol-level primitive for “these people can access this stuff”. When you post into a bucket, your post inherits the ACL of that bucket. When someone is added to a bucket, they get access to everything in it. When they’re removed, they lose it.

A bucket is a bit like a repository. But it isn’t the public repository and (spoiler) probably doesn’t use an MST. Users will have many buckets, one for each social context that they are creating content in. A bucket holds records of many different types and from many members. For instance, a single bucket may contain all content for a given AtmoBoards forum.

To quickly compare buckets back to realms: realms were abstract data partitions that described the “type” or intended use of some data. For example, “AtmoBoards forums content”. A bucket describes a particular shared social context with defined owner, admins, and members. For example, “the Protocol Nerds forum on AtmoBoards”.

Buckets give us a few things we’ve been missing. They provide a natural unit of access control that is neither too granular (per-record) nor too coarse (per-app). They handle dynamic membership. And they give applications something concrete to sync and index.

One thing worth flagging upfront: a bucket doesn’t necessarily imply a physical container sitting on one PDS. Consider how threads work in the Bluesky app today. Each post lives on its author’s PDS and the thread is compiled by the application by virtue of the fact that each post references the same root. A bucket could work similarly, with its contents distributed across members’ PDSes rather than centralized on one host. There are real tradeoffs here and we’ll dig into them in the next post.

And there’s still a lot more to figure out. How is bucket data actually stored/represented in the PDS? How is bucket data addressed? How is bucket ownership managed and can buckets be transferred? How is access to a bucket represented and enforced? Who has access to buckets: users or applications? How do applications sync buckets in real-time?

We’ll get into some of those in the next post. For now, the key insight is this: permissioned data needs a shared context with a perimeter. And that space needs to exist, not just in the application and not scattered across individual users’ PDSes, but at the protocol layer.

https://dholms.leaflet.pub/3mfrsbcn2gk2a
Permissioned Data Diary 1: To Encrypt or Not to Encrypt
The first in a series of posts about major design decisions along the way to a permissioned data protocol for atproto.
Show full content

We’re working on permissioned data for atproto! The design space is still open, but we’re starting to gain confidence in a few core ideas that help structure the solution. We have a rough goal of having a rough proposal by AtmosphereConf (statements I won’t regret for 500, please). 

I wanted to write up and explain some of our design decisions in a series of posts as we go along. This is the first in that series and is about our decision to not design an end-to-end encrypted (E2EE) system.

But first, let’s discuss what “permissioned data” actually is. Permissioned data is a broad term, it covers many different social modalities & data flows. In its most basic sense, it means “not public”. In other words, data that lives on your PDS but isn't broadcasted out over the firehose and is only accessible to users and services that have been explicitly granted permission.

Some non-exhaustive examples:

  • Groups: Facebook groups, private forums, private subreddits

  • Socially shared: private posts, Instagram reels

  • Gated content: Patreon, Substack, paid newsletters

  • Personal data: mutes, bookmarks, drafts

  • Messages: DMs, group chats

DMs are different

DMs are the clear outlier of that group. Every state of the art messaging app has converged on E2EE as table stakes. Signal did it, WhatsApp adopted the Signal Protocol, iMessage has it, even Facebook rolled it out on Messenger. If you’re building a messaging product in 2026 and it’s not E2EE, you have some explaining to do.

But looking at the rest of the list, nobody expects a private subreddit to be E2EE or for their Patreon subscription to function by negotiating key material with every paid subscriber. 

The threat models are different. When you send a DM, you’re thinking about confidentiality in the classic sense. You want to be certain that only you and the person(s) you’re talking to can read the message. The adversaries you’re worried about are the server operator, a state actor with a subpoena, or a hacker who compromises the infrastructure. 

However, when you post in a private subreddit with 50,000 members, you’re not worried about the server operator reading your post. Your goal isn’t to keep the content secret, it’s to keep unauthorized users from viewing the content. In other words, you’re thinking about access control, not encryption.

Apps need to see the data

In fact, in many cases you want the server to read your post.

One of our fundamental design postures with atproto is to take “no steps backward”. We want to enable social experiences that feel as good or better than traditional social networks, in a decentralized manner.

At this point, users expect to be able to search within a group, get notifications, see aggregate views (“trending in this community”), have sensible moderation tooling, get recommendations, and more. All of these features require backend services to read, process, and index the data. 

In an E2EE system, only clients get access to the underlying data and then have to construct indexes locally on the client. This works well for DMs and some limited social experiences. But modern big world social really can’t be done solely in the client. The reasoning here is actually pretty similar to the reasoning for why atproto isn’t a p2p protocol - it really benefits from having modern backend infrastructure.

E2EE is hard

As much as I would like to spend my next year working on an MLS implementation and the ensuing key management issues (not being sarcastic), the reality is that E2EE is hard. And this inherent complexity isn’t something that the protocol team at Bluesky can just handle - it gets pushed out to every dev trying to build a client that works with encrypted data. And boy howdy if you thought OAuth was a pain.

Real humans lose devices, get new phones, forget passwords, and have exactly zero interest in managing cryptographic key material. The messaging apps that have made E2EE work have poured massive amounts of effort into making key management invisible, and it’s still a source of friction. Now extend that to every group, forum, newsletter, and private account across an open ecosystem of heterogeneous clients.

There are also just inherent scaling limitations to E2EE systems. The state of the art for group E2EE (MLS & friends) is actually really good at this stuff and can scale to thousands of members. The last I checked, current algorithms were designed for (theoretically) going up to 50k participants but most implementations cap the group size much lower than that (like 2-10k). That’s good, but many permissioned spaces on the internet are much bigger than that. We want to design for spaces that can scale up to hundreds of thousands or millions of members. 

Layering encryption on top

All that being said, this isn’t an either/or decision. Permissioned data and E2EE aren’t actually competing approaches - they operate at different layers.

Permissioned data is about access and data flow. Who is allowed to see this data? How does it get from point A to point B? How do applications and services engage with data on behalf of their users? What is the addressing space for non-public data? 

E2EE is about cryptographic confidentiality. It ensures that even if someone intercepts the data or compromises the infrastructure, they can’t read it.

You can layer the second on top of the first. Messaging certainly benefits from E2EE. But other social modalities can benefit from it in certain contexts as well. I’d love to see an E2EE forum or community space emerge at some point. Ultimately it’s each application’s prerogative to determine what the threat model is for its social modality and to adopt the relevant security posture.

For this project, we’re going to be focusing on designing a permissioning and data protocol that enables modern big world social and scales to millions of users. Stay tuned for more updates as we continue our work on this!

https://dholms.leaflet.pub/3meluqcwky22a
PLC Threat-modeling & Auditability
Show full content

I’ve been meaning to break in my leaflet for awhile but haven’t gotten to it! Thanks to Phil (@bad-example.com) for voicing some concerns about the current work that we’re doing on PLC to support mirroring. It gave me a good reason to write up my thoughts on the technical improvements we have planned for PLC & to invite feedback/discussion from folks that are interested.

First some background: DID PLC is a self-authenticating DID which is strongly-consistent, recoverable, and allows for key rotation. Each identity is made up of a series of signed operations that each reference the preceding operation in a chain back to the genesis operation. Any observer can verify the chain of operations in order to verify the current state of the identity. 

PLC operations are stored in a directory (currently run at plc.directory) which is a centralized service that validates operations, stores them, and serves those operations as well as computed views of the identities derived from the operations. The trust model for the directory is fairly limited as it is unable to modify identities without a signed operation on their behalf. 

The directory cannot fake a valid operation, however the directory may reject a valid operation or remove a previously accepted operation. In other words, the directory must be trusted with “maintaining the set of accepted valid operations”.

One design goal of PLC is that applications should have a clear answer about the current state of an identity. If two applications reach out to the same directory, they should always receive the same answer.  This sort of strong consistency requires either a single writer or a consensus mechanism. PLC currently achieves this through a centralized single writer. 

Let’s first look at alternative consensus mechanisms and then look at checks and balances if we maintain PLC as a single writer.

In terms of consensus, we have two options: an open or closed set of participants. An open set of participants requires some sort of sybil-resistant BFT consensus mechanism a la blockchains: Proof of Work, Proof of Stake, etc. This in turn requires some sort of incentive mechanism (usually a digital currency) that encourages reliable participation. To me, this is just a non-starter. I really don’t think PLC should have a cryptocurrency, and I really don’t want the UX hits of operating off a blockchain (paying for transactions, awaiting block confirmations, etc).

The other type of consensus is to have a closed set of participants that use some consensus protocol like Paxos in order to decide on the current state of the directory. Of course this just moves the governance problem from “which operations are included in the directory” to “which organizations are included in consensus”. The world has sophisticated governance and legal structures for handling exactly this type of multi-stakeholder decision making, and my opinion is that those structures are better suited to the job than Paxos. In other words: a single organization governed by a multi-stakeholder board (which is what we’re pursuing btw).

Okay but even a multi-stakeholder, independently-governed PLC can go bad. So what does the network do in that case? My opinion is to solve this in the way that we commonly solve it in AT: provide credible exit. That is, if you have reason to believe that a directory is not acting in good faith, then you have the ability to use another directory. You’ll notice that all of the reference client SDKs for PLC allow you to swap out the URL of the directory that you’re talking to. 

Applications or communities may run their own directories. In nearly all cases those directories will directly mirror plc.directory. If plc.directory chooses to exclude an operation that other directories deem valid then they can opt to include the operation and even gossip it around to other directories while continuing to mirror the operations coming out of plc.directory.

Currently we’re working on a feature to make mirroring of PLC easier. This occurs over a streaming websocket and involves assigning a monotonic sequence number to each operation in the directory. This is a step toward our longer goal which is the introduction of Transparency Logs (tlogs) to PLC (see Sunlight for more on that). Tlogs require strong ordering of operations within a directory.

The concern of giving operations a sequence number and tlogs more broadly is that this may complicate a multi-authority future. If an alternative directory accepts an operation that plc.directory does not, then those two directories may be required to maintain different sequence numbers for all operations going forward. In any case, they will never have the same root hash for their tlog. If that sounds uneasy or messy, it’s because it is.

My opinion is that we should own up to the messiness of this. We shouldn’t consider either the sequence numbers of operations or the root hash of a tlog to be a “global” value or something that divergent directories must reach consensus on. Rather these should be thought of as mechanisms that allow for auditing a particular directory. As discussed earlier, the responsibility of a directory is to decide “which operations are included in the directory”. Sequence numbers and tlogs allow observers to audit exactly how a given directory answers that question.

This does not mean that a multi-authority future is not messy (and hopefully it never happens!). But in that future, the most important thing to resolve that messiness will be to understand exactly where two directories diverge from one another.

As I mentioned, I’m looking forward to hearing any feedback/concerns around this ahead of rolling out the websocket export API!

https://dholms.leaflet.pub/3m6zswymcqk2p