The Arbiter - Group Management for Permissioned Spaces and Beyond

The Bluesky team has recently announced their rough plan for private data on ATProtocol with their Permissioned Data Proposal . This is a huge deal!

About a year ago, when we first started making @roomy.space , private data was the single biggest reason we couldn't make Roomy fully "on-protocol". After several iterations we ended up with our own data server so that we could store private data and selectively grant access to members based on their ATProto identity.

But after a year of getting closer and closer to ATProto's design decisions, we've finally decided to go all in on trying to make Roomy work on ATProto. This includes private data!

The proposal we have from Bluesky right now is really just a rough draft. It's in the "Permissioned Data Diary", a set of notes to get the ideas out to the public. It will almost surely change, but we want to be there for it so that we can help shape it and get it to the point where we are confident building Roomy on top of it.

To that end, while @meri.garden is working on core features and performance improvements for the current version of Roomy, we've also been discussion the next ATProto-native iteration of Roomy, and I've been speccing out the first piece of the new design: the arbiter.

Previous Discussion

Before getting to this point me and @meri.garden had a live-streamed call that you can watch the recording of where we talked about the start to this design.

Meri also left some feedback on the permissioned data proposal, which got some a response from @dholms.at :

Evaluating permissioned spaces for community contexts - meri's missives

A look at Bluesky's proposed approach to private data on ATProto

Permissioned Spaces & The Member List

The permissioned data proposal has this idea of permissioned spaces. Each space has an owner DID managed by the space host. The host is responsible for managing the member list.

If you are on that member list, then you can ask the host for a space credential and it will give you a temporary access token that can be used to get data in that space.

All the data in the space is stored on the members' PDSes, but if you present that space credential, which is signed by the host, they will know that you should be allowed to read the private data.

Managing the Member List

Membership management can be complicated, though. Who is allowed to add and remove members from the space? Can anybody join? Do we have invite links?

There are many questions and the permissioned data proposal wisely doesn't attempt answer all of them in the protocol itself. Instead, it allows the space host to delegate management of membership to a managing application. This could have any kind of API that the application might need. The only requirement is that, in the end, the space host is able to get a list of members so that it knows who to grant space credentials to.

Interoperable Group Management

But what if we could have an interoperable group standard? We could allow space membership to optionally be published publicly. Then we could use the public list as a way to share group membership info with other apps.

If we had an API for delegating space access to other groups, then it would allow for exciting new cross-app integrations.

For example, if the Leaflet team published a public group with all their team members, we could create a private Roomy channel that automatically allows anyone on the Leaflet team to view and chat in the channel.

The Leaflet team maintains control over their group, but we can delegate access in our app to members of their group.

Almost daily we are seeing some new use-case for standardized groups on ATProto, and we really want to unlock this in the context of permissioned data on ATProto.

The cool part is that this doesn't require any changes to the permissioned data proposal! It was designed to work with this kind of solution, and we are just providing one possible solution for member list calculation.

The Arbiter

That is where the motivation the arbiter comes from. It can provide the membership list functionality needed for permissioned spaces, but instead of being application-specific, it will have a standardized XRPC API that can be used by any ATProto app.

It also doesn't need permissioned spaces to work. If you just want a system for calculating and delegating group management, you can use it for that alone.

How It Works

If we are making a generic group membership calculator we want it to be pretty flexible, but we also don't want to overcomplicate.

Over the last week we've been iterating on details and we now have a design specced out that I think might work really well.

Lets go over the details.

The Arbiter as a Community Boundary

Our arbiter implementation will serve directly as a space host, and it represents the community / organization boundary.

When you create a new arbiter, it will create a DID that will be the root of authority for the community.

If you want, you can download a rotation key that you can save for later, in case you ever need to migrate away from a malicious arbiter.

While the permissioned data proposal allows for users to create spaces under their own DID, this has the significant caveat that the community is permanently tied to their own identity and can't be handed off to new management without handing off all the user's personal data, too.

So while not a hard rule, generally you will setup a new arbiter for each community that may want to be separately governed, and each of them will get their own DID.

Roles are Groups, which are Spaces

You can create any number of roles, groups, or spaces in the community. While these often have different use-cases, the arbiter doesn't distinguish between any of them and they are all treated as a space according to the permissioned data proposal.

For example, you might create a #general channel in Roomy, which then has its own permissioned space on the arbiter.

You might also have a team role or a moderator role. Those would also be spaces.

As far as the arbiter is concerned, these are all just spaces that we need to be able to calculate a membership list for. Even though they are spaces, there's no reason you have to write records to them if the only meaningful thing about them is the membership list.

From now on, I'm going to use the terms "group", "role", and "space" somewhat interchangeably, based on how a specific space is meant to be used, even though they're all just spaces.

Spaces Can Delegate to Other Spaces

One of the key elements of the system is that spaces can delegate membership and access to other spaces.

Every arbiter has one special $admin space that always exists, can never be deleted, and is automatically added with access to every other space on the arbiter. Members of this space can also be granted access to create new spaces.

In turn, since $admin is just a space, it can delegate access to other spaces, even if they are on a different arbiter.

For example, we could create a community for Muni Town, where me, @meri.garden and @erlend.sh are owners in the $admin space.

We could then create a new community for Roomy and have the $admin space in the Roomy community delegate access to the admins in the Muni Town community.

This means that every time we add or remove an admin from Muni Town, they are automatically have that access added or removed to the Roomy community!

Motivating Use-Case: Federated Channels

Space delegation unlocks one of my favorite feature ideas for Roomy: federated channels.

A federated channel would be chat channel that can exist in multiple different Roomy communities.

For example, if we had a Muni Town community with #general channel, it would be really cool, if that was actually the same #general channel in the Roomy community.

This can be an awesome way for multiple communities that trust each-other to reduce the walls between them and help members discovery each-other and find more potential shared interests! It's kind of like the chatroom version of a Webring !

In this case, the #general channel would exist in the Muni Town space, but would add the members group of the Roomy space as a member of the #general channel. That means that anybody who joins the Roomy community, automatically has access to the Muni Town general channel.

Once that access is granted, the Roomy community can add Muni Town's #general channel to their sidebar so that you can chat in it from either community!

Importantly, Muni Town, the owner of the channel, still maintains control, over the channel and can revoke Roomy's access if they ever need to. And the Roomy community can always decide they don't want to list Muni Town's general channel in their sidebar anymore.

Both community's maintain ownership and control without being at the mercy of the other.

How Delegation Works

Space delegation is handled simply by saying that one space is a member of another space, just like a user could be a member of a space.

For example, you might have a team role and a moderator role, and you could say that the moderator role is a member of the team role. That means anybody added to the moderator role is automatically included in the team role's member list.

The arbiter will automatically traverse the membership list, resolving the members from other spaces if needed, to create the final list of all the users in a space.

It also allows you to control the access levels that are granted to different users or spaces.

Access Levels

For each member0 in a space, the Arbiter keeps track of an access level. Importantly this access is not an application-level concept. The access level only controls what the user or group can do on the arbiter.

For example, the arbiter allows you to create a group such as moderators, but it's not up to the arbiter what the moderators can do in your app. That is up to your app to decide and enforce.

But the arbiter does need to track access levels so that it knows who can modify the spaces and their membership.

The arbiter has 8 different access levels. Each level builds on the permissions of the lower levels, granting access to all the permissions below it.

That would mean that you could add other people to the community space, and you could have moderator access in your app, but you wouldn't have permission to add new members to the moderators role.

The full list of access levels is:

Space Configuration

There are two different ways to "configure" spaces: changing the public access settings, and writing records the arbiter's account.

The ability to change both of these is granted by having Configure Space access.

Public Access Settings

These settings are useful for making things like public forums or groups.

Membership is Public: Enabling this allows anybody to query the membership list of the space, even if they are not a member.

Records are Public: Enabling this allows anybody to obtain a space credential and read all the records in the space, even if they are not a member.

Writing Records Under the Arbiter's Account

Users with Configure Space access can also write records to the space in the arbiter's permissioned repo. This is a very powerful that doesn't come with many safeguards and should be granted only to trusted admins.

This feature allows you to add any other kind of metadata that you might need to add to space, where members can be sure that the data is valid, because it is written and signed by the arbiter itself.

This functionality could also be used by apps to store application-specific configuration, such as which app permissions are granted to different roles.

This is convenient because the space host / arbiter is the main entry point for a community, and the root of trust in the community. By giving the arbiter it's own repo, different apps can know where to go to get important, authoritative configuration. It can provide an answer for who is allowed to do what in communities that require more than just a member list.

It is important to note that every user with Configure Space access will be able to write and delete records under the arbiter's account in that space. There is no mechanism to restrict which records they may write or whether they can delete, etc. It's all or nothing in the space that they have been granted access to.

Invites

Invites can be managed by a separate invite service that we will also make a standard API for. It will be an optional add-on that can be enabled by adding the invite service as a member to your space with Add Members access.

Summary & Implementation Progress

This idea is less than two weeks old, and has been progressing rapidly in our discussions and experimentation. We are excited that the permissioned data proposal actually looks like it will work for us, and the arbiter is the first piece on top that we will need to make it feasible.

We are also hoping that other ATProto apps might be able to find this useful as a standard for group / role management as permissioned data becomes more common and standardized.

I have most of the core functionality implemented already in a @quint-lang.org specification, and I want to talk about that more, but this post is already really long, so I'll start writing a new post instead. 😁

Also note that some of details of this design are new since about… 6 hours ago when I should have been sleeping, so things could definitely change! I think this is a really good start, though, and I can't wait to get it running so we can test it out! 🚀

Addendum

After some discussion with @flo-bit.dev there are a few other ideas would be good to integrate into the arbiter:

Having a special $publish space where, if you are a member of it with Configure Space access, you can write records to the public ATProto repo under the arbiter's DID.

This only works if the arbiter has public repo capabilities. Not all arbiter implementations would be required to support this.

It was noted that we didn't specify exactly how member lists are resolved from remote arbiters, for example whether syncing the member list is push-based or pull-based.

I think we should start of pull-based with caching and a TTL similar to DNS. But the core state machine that is being implemented doesn't care how remote lists are resolved or kept up-to-date, so this is something that can be iterated on. For now I think we focus on the simplest thing that works and see how it plays out in practice.

Another thought is that it makes sense for the arbiter to be able to "adopt" existing DID.

By giving the arbiter an app password to an existing PDS account, the arbiter can use that PDS's public / permissioned repos instead of having to implement PDS features itself, or having to create and manage a new DID signing key, etc.

It would also be optional to have a new $labeler space, that allows you to create labels if you are added to the space with Configure Space permission.