Proposal for Atmosphere Groups standard on atproto spaces
proposal proposal.md
19 kB

opensocial.group proposal #

This document specifies the proposed opensocial.group model. See the project README for an overview, background, and design posture.

An atmospheric group is a DID with a bunch of permissioned spaces under it. There is a one-to-one mapping between groups and DIDs.

opensocial.group is a set of lexicons describing space types, record types, and methods for modeling and interacting with atmospheric groups.

Groups run on opensocial.group-compliant group hosts. Groups should be able to migrate from one group host to another without disrupting how members interact with the group.

opensocial.group is a peer of the simplespace implementation, which PDSes are required to implement. It is not expected to operate on most PDSes and does not layer on top of simplespaces.

Group hosts #

A group host functions like a PDS for group accounts. Just as a person's PDS hosts their account and data, a group host manages the group's network identity, hosts and signs its public repository and space repositories, and acts as the space host for the group's spaces.

It adds the group-specific functionality needed to manage a collectively controlled account. It understands the opensocial.group records and methods, enforces space access policies, and checks roles when authorizing group-account OAuth scopes. It is both the authorization service and the repo host for records authored by the group.

Members continue to publish their own records to their own PDSes, including records in the group's spaces. Members' repositories are not stored on the group host.

The group host serves as the OAuth authorization server for the group account. Users authenticate to it using their personal atproto accounts, and the host determines which group-account scopes they may authorize from their roles. This is described in the OAuth flow below. A user's account may be on a different PDS, and that PDS only needs to support the standard atproto OAuth flow for authenticating them.

The spaces #

There are two opensocial.group spaces:

  • a meta space which includes metadata about the group that is often shared publicly rather than just with members. This includes the profile, description, rules, group guidelines, etc. This space has to do with “overall group presence & discoverability.”
  • a members space which includes members-only information about the group. This includes a member list, member confirmation/acceptance, roles, and an index of available modality spaces. This space has to do with “roles and access.”

The opensocial.group spaces are specifically intended for records about the group. modality- or app-specific data is not published to these spaces.

Rather, a group hosts an additional space for each application or modality. If a group wants to enable event applications, it might create a group.lexicon.calendar.events space. All group.lexicon.* records will then be published into this modality-specific space rather than into a universal “group” space.

The members space indexes the group's spaces with a space record per space. The index is a discovery aid, not an exhaustive list. A group may omit a space, for instance so that members cannot learn an admin-only space exists.

Roles & Access #

Authorization & access is modeled as flat RBAC (role-based access control). The group defines many roles and assigns one or more roles to each member. Each role is granted a set of OAuth scopes on the group DID.

There are no deny rules or caveats. So authorization composes simply by unioning all scopes granted by a member's roles. There is no precedence, hierarchy, or evaluation order.

Membership is defined bidirectionally. The group creates a membership record in the members space and the member in turn creates an acceptance record in the same space.

The group DID itself is implicitly a member with all group permissions. It does not need its own membership or acceptance records or assigned roles.

Scopes cannot express which roles a user may grant, revoke, or eject, so each role also has an assignable list. When a user writes a membership, every role added or removed, and every current role of a member being removed, must be in the union of assignable across their roles.

Roles may entail certain conventions. For instance, the Bluesky app will likely special-case the roles admin and moderator and give special UI affordances/badges to accounts with those roles in a group. Other roles may be treated as flair or metadata. A group is free to ignore the convention.

Scopes are granted in two places:

  • Each space in the group (both modality-specific spaces & opensocial.group spaces) has an access record that grants each role scopes for that space. A role granted read on a space may obtain space credentials for it, which the space host enforces. The record can also make the space public.
  • The permissions record in the members space grants scopes that are not tied to one existing space: manage=create, rpc: scopes for the group methods, and grants across all spaces, such as space:* for writing labels. It also holds each role's assignable list.

Apps determine what a member may do by reading their membership, the roles' grants in access and permissions, and each role's assignable list.

opensocial.group publishes permission sets for common grants, which roles and applications can use instead of listing scopes.

Permissions for modality-specific actions are app features. Who can write to a space, who can pin a thread, who can post to an announcements channel, who can create an event, etc. Each of these is declared in the modality’s own lexicon within the relevant space. These authorization declarations may use the group-defined roles.

Writing as the Group DID #

The group DID authors the group's profile, rules, roles, permissions, space index, access records, memberships, invites, and labels. It can also author modality-specific records. For example, pinning a post at the top of a forum may require the group DID to create a pin record in the forum space.

Applications create, update, and delete these records through com.atproto.space.*, using an OAuth session for the group DID. The host checks that each write follows the semantics for that record type and that the session permits it.

Authorization uses a two-sided OAuth flow:

  1. The application starts an atproto OAuth flow for the group DID, requesting the scopes it needs.
  2. The group host authenticates the user by acting as an OAuth client to their personal PDS in a second flow (requiring only authentication).
  3. The group host checks the user's roles and presents consent for the scopes those roles allow.
  4. On approval, the application receives an OAuth session for the group DID.
Application                     Group host                       User's PDS
    |                                |                                |
    |--- OAuth for group DID ------->|                                |
    |                                |--- OAuth for user DID -------->|
    |                                |      (authentication only)     |
    |                                |                          User signs in
    |                                |<-- Personal OAuth session -----|
    |                                |                                |
    |                   Check roles + user consent                    |
    |                                |                                |
    |<-- Group OAuth session --------|                                |
    |                                |                                |

Applications can use these sessions in two ways:

  • Per-user sessions. Each user authorizes a session for their own actions, using the scopes allowed by their roles. This suits personal tools and independent clients.
  • Shared application sessions. An admin authorizes a session that the application uses on behalf of multiple members. This avoids a separate group authorization flow for each member when many members use the same trusted application. The application is responsible for checking each member's permissions before using the group session on their behalf.

The group host must track which user authorized each session. Some permissions depend on the record being written, and OAuth scopes cannot express theses limits. For example, assignable limits which roles a user can grant or revoke and which members they can eject. On every write, whether through CRUD or a method, the host checks them using the current roles of the user who authorized the session. For shared sessions, this is the authorizing admin.

A session's scopes are likewise limited on every request to what its authorizing user's current roles grant. If that user loses roles or membership, the session narrows accordingly.

A user can authorize a scope only if their roles grant it.

Methods for personal actions, such as joining, leaving, or reporting, use service auth from the person's own account instead of a group-DID session.

Creating a group #

An application creates a group by starting the OAuth flow at the group host with prompt=create. The host authenticates the creator through their PDS, creates the group account, and returns a group-DID session. This is how an application may allow a user to sign up for an account on a PDS.

Each group host decides who may create groups and under which circumstances. For example, a group host may have a relationship with a particular app or require invite codes.

A new group starts with:

  • a DID and handle
  • the meta and members spaces, each with an access record
  • a profile and a permissions record
  • a membership for the creator, with a role that grants every scope and can assign every role

Particular role semantics are not required, though common practice may be for hosts to default to an admin role for the creator and a member role that can read the meta and members spaces.

Presence & Discoverability #

Each group has a profile that it publishes in its meta space. This includes some basic metadata about the group such as a display name and a profile picture. It also includes records describing the rules of the group.

Any app that wants to show or interact with open social groups will use these profile records when displaying the group.

Many groups will wish to make their meta space public, even if the rest of the group is private. Even in those cases, the data is still published in a space; the space is just permissioned to allow public reads.

Groups that wish to be easily discoverable may also publish a declaration record to the public broadcast protocol. This record simply includes a pointer to the public meta space for the group.

Moderation #

Each group is also a moderation service with moderation authority over all content in the group.

Moderation happens in the normal atproto way: reports come in one side, labels come out the other side. In some cases, certain group-specific actions such as ejection from the group may need to occur.

Reports are sent through group.opensocial.createReport.

Labels are records that the group DID writes in its own repo, in the same space as their subject. A label may reference an account or any record in that space, by any author, but not content in another space, so exactly those who can read the subject can read its labels. Labels on accounts are published in the members space. Deleting a label record negates it. We will likely define a new com.atproto label record type. The host keeps a moderation history of label writes and member ejections.

The group defines its own label values in a labeler record in the members space. It declares the label values and their definitions, plus the report reasons and subject types the group accepts.

On top of this, a simple moderation information architecture is specified. In this system, a report may be open, escalated, or resolved. Moderators may see limited information and take limited action on reports. The goal of this system is to support in-app moderation use cases.

Groups at scale may support significantly more complex moderation systems. However, this will likely require bespoke group moderation software and wouldn’t be done through in-app flows.

Invites #

An invite is an interesting case because it must reach someone who isn’t yet a member and therefore does not necessarily have access to the group’s spaces. Invites usually should not be broadcast publicly.

Instead, each user who is interested in receiving invites to groups hosts an invites space under their own DID, and a group writes an invite record into that space. The group will notify the user’s PDS of the write, and the PDS will in turn forward that notification to any relevant services that can then read and present the notification to the user.

Only the user (and authorized applications) may read the space. In other words, the inviting group writes the record but can’t read other invites in the space.

The group writes and deletes invite records in its own repo through the standard CRUD methods. Deleting an invite revokes it.

Migration #

A group migrates to a new host through the standard account migration flow, including the migration of its space repos. Everything the group stores in repos, including labels, moves with it.

Private host state does not migrate. This includes join requests, the moderation queue and history, and OAuth sessions, which users authorize again on the new host. A host may offer an export of its moderation history, but opensocial.group does not define one.

Group governance #

The opensocial.group standard is intended to standardize the interface between applications and groups. Therefore, group governance is generally considered out of scope.

A group may implement arbitrarily complex governance, including things like voting and holding periods for certain actions. This is all considered out of scope for the group standard.

Spaces, records, and methods: at a glance #

Space types #

Space type skey Purpose
group.opensocial.meta self The group's public face. Profile, rules, how to get in. Often readable by anyone, though may be private to members.
group.opensocial.members self Roles, who holds them, the authz config, and the space index. Usually gated to members.
group.opensocial.invites self Hosted by each user, not by groups. Where invites arrive.
(modality spaces) any com.atmoboards.forum, app.bsky.group, etc. Not specified here.

Record types #

Record Space Author Key Purpose
group.opensocial.declaration public repo authority self Marks a DID as a group. Points at the meta space. Discovery only.
group.opensocial.profile meta authority self Name, description, avatar, join policy.
group.opensocial.rule meta authority tid One group rule with a stable URI a mod action can cite.
group.opensocial.labeler members authority self The group's label value definitions and accepted report reasons.
group.opensocial.permissions members authority self Group-wide scope grants and each role's assignable list.
group.opensocial.space members authority tid Lists one space in the group's index.
group.opensocial.role members authority role id Declares that a role exists.
group.opensocial.membership members authority member DID Grants a member their roles.
group.opensocial.acceptance members member self The member's side of membership. Gates whether the member appears in the roster, not access.
group.opensocial.access any space authority self Scopes each role is granted for this space, including read, and whether it is public.
(label, com.atproto) any space authority tid A moderation label written into the space its subject lives in.
group.opensocial.invite invites inviting group tid An invitation delivered into the invitee's own space.

Permission sets #

opensocial.group publishes these permission sets for group-DID sessions. Each covers the records written in the group's own meta and members spaces, or in invitees' invites spaces, plus the related methods.

Set Writes Calls
group.opensocial.configure profile and rule in meta; role and permissions in members —
group.opensocial.manageSpaces space index entries in members; access in meta and members —
group.opensocial.manageMembers membership in members; invite in invitees' invites spaces listJoinRequests, admitMember
group.opensocial.moderate labels in meta and members listSubjects, getSubjectHistory, resolveSubject

A permission set must name a specific space type, so grants that span the group's modality spaces are raw scopes.

Grant Scope
Write labels in any group space space:*?collection=com.atproto.label.label
Write access records in any group space space:*?collection=group.opensocial.access
Create and delete spaces space:*?manage=create&manage=delete

Methods #

Methods handle data stored privately by the host, space creation and deletion, and actions users take as themselves (not as the group).

Method Auth Requires Purpose
createSpace group-DID session manage=create Creates the space with its access record and optionally an index entry.
deleteSpace group-DID session manage=delete Deletes the space and its index entry. Cannot delete the meta or members space.
listJoinRequests group-DID session rpc: scope Lists pending join requests, which the host stores privately.
admitMember group-DID session rpc: scope Resolves a private join request and writes the membership.
listSubjects group-DID session rpc: scope Lists the moderation queue, which the host stores privately.
getSubjectHistory group-DID session rpc: scope Returns a subject's moderation history, which the host stores privately.
resolveSubject group-DID session rpc: scope Resolves or escalates a subject in the host's private moderation records, optionally with a note.
requestJoin member service auth — Creates a private request or redeems an invite for a non-member.
cancelJoinRequest member service auth — Withdraws a private join request.
leaveGroup member service auth — Deletes the caller's membership record, which is authored by the group.
createReport member service auth access to the subject's space Opens or reopens a subject in the host's private moderation records.