# opensocial.group proposal This document specifies the proposed `opensocial.group` model. See the [project README](README.md) 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](https://github.com/bluesky-social/proposals/tree/main/0016-permissioned-data#required-pds-space-management-simplespace), 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 `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. ## 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. |