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.
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 space hosts. These are servers that natively understand the standardized records & methods. Groups should be able to migrate from one space host to another.
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.
The spaces #
There are two opensocial.group spaces:
- a
metaspace 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
membersspace 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.
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 gives access to a set of standardized actions.
There are no deny rules or caveats. So authorization composes simply by unioning all actions authorized 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.
Two actions — assigning a role to a member and ejecting a member — are parameterized by the roles that they apply to.
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.
Each space in the group (both modality-specific spaces & opensocial.group spaces) has an open social access record. This record specifies which roles are able to view the space. Read access is enforced by the space host.
All other types of access are considered modality-specific and as such 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. However, these authorization declarations may still make use of the group-defined roles.
Writing as the Group DID #
Certain modality-specific records need to be created by the group DID. For instance, pinning a post at the top of a forum may require a pin record created by the group DID in the forum space.
If a user (for instance, a group admin) wishes to write as the group DID, they do so by holding an OAuth credential for the group DID and writing that record to the group service. The group service will support all com.atproto.space.* CRUD methods.
This does not imply that the user needs to have “full access” to the group account or that they need to log in to the app as the group account in a traditional sense. Rather, the group service also serves as an OAuth authorization server for the group DID. Instead of requiring a password, it requires that the user authenticate using their personal account.
The OAuth scopes that each user/role is able to request for the group DID are also encoded in the access record in each relevant space.
Applications are in charge of juggling these credentials. They may choose to offer an “account switcher” where an admin switches to using the group account, or they may do this more transparently and just use the group account credential when the admin performs some action that requires it.
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 a createReport method in a similar manner to how content is reported to a moderation service.
Labels are published as records by the group DID in the same space as the content. Labels on accounts are published in the members space. We will likely define a new com.atproto label record type.
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.
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.permissions |
members | authority | self |
The authz config. Binds roles to actions and bounds role.assign. |
group.opensocial.space |
members | authority | tid | One space under this group. The index of what exists. |
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 |
Who may read this space. |
group.opensocial.label |
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. |
Actions #
| Action | Governs |
|---|---|
mod.read |
see the moderation queue and subject histories |
mod.resolve |
resolve or escalate a subject, add notes |
label |
apply and negate labels (excluding !hide and !takedown) |
takedown |
apply and negate !hide and !takedown labels |
invite |
issue invites |
admit |
approve join requests |
eject |
remove a member, bounded by assignable |
role.assign |
grant and revoke roles, bounded by assignable |
space.create |
create a space under the group DID |
space.configure |
change space config, including its access record |
space.delete |
delete a space |
group.configure |
edit the profile, rules, roles, and permissions |
Methods #
| Method | Requires | Purpose |
|---|---|---|
updateProfile |
group.configure |
Replace the profile: name, description, avatar, join policy. |
uploadImage |
group.configure |
Upload an image for the profile avatar or banner. |
putRule |
group.configure |
Create or update a rule. |
deleteRule |
group.configure |
Remove a rule. |
putRole |
group.configure |
Create or update a role and its action bindings. |
deleteRole |
group.configure |
Remove a role. |
createSpace |
space.create |
Add a modality space to the group. |
updateSpace |
space.configure |
Change a space's config, including who can read it. |
deleteSpace |
space.delete |
Remove a space. Refuses to delete either of the two well-known spaces. |
assignRoles |
role.assign |
Set a member's full role set. |
ejectMember |
eject, bounded by assignable |
Remove a member and their access. |
createInvite |
invite |
Write an invite into someone's own invites space. |
listInvites |
invite |
Outstanding invites. |
revokeInvite |
invite |
Retract an outstanding invite. |
requestJoin |
— | Ask to join or redeem an invite. |
cancelJoinRequest |
— | Withdraw a pending join request. |
leaveGroup |
— | Leave the group. |
listJoinRequests |
admit |
The pending-requests queue. |
admitMember |
admit |
Approve or deny a join request. |
listSubjects |
mod.read |
The moderation queue. |
getSubjectHistory |
mod.read |
Every event on one subject. |
resolveSubject |
mod.resolve |
Resolve or escalate a subject, optionally with a note. |
applyLabel |
label, or takedown for !hide and !takedown |
Write a label record into the subject's space. |
negateLabel |
label, or takedown for !hide and !takedown |
Negate a previously applied label. |