Groups are a shared context for communications organized around bounded membership in a set of group members. Membership in a group is distinct from following the group; being a follower entails potentially receiving future activities published by an actor, but being a member entails a social role within the group independently of whether activities are received. This document explores ways to create and manage groups within the ActivityPub ecosystem.

Introduction

What is a group?

In a social setting, activities can be bound to various contexts. These contexts can be things like:

Popular implementations of ActivityPub (and popular social networking and social media services) have unfortunately historically neglected support for binding resources to context, in a problem known as context collapse. In particular, the social ecosystem widely lacks primitives for posts having authorship and audience bound to a consistent but dynamic collective group.

In social psychology, groups are established based on a shared purpose or identity that mediates group activities. For example, users hosted on the same network or service do not necessarily share any common identity. You could describe a "collection of all users on this service", but this is not necessarily the same as a "group". The quality that defines a "group" is its entitativity, or the perception of being a group (from [[[understanding-social-groups]]]). It is only when the Group becomes a "real" "entity" (or is "reified") that we can consider it a Group (and not simply a Collection of actors). We can describe properties of the group separately from properties of its members, and separately from properties of the collection.

Similarly, membership in a group entails its own properties within the group context, separately from the properties of the individual who is a group member. For example, you might have permission to perform certain actions in one group, but not in another. It would be imprecise to say that the individual holds those privileges; rather, the member holds those privileges within the group context, and that membership is a social role performed by the individual. Outside of the group context, membership considerations do not apply to the individual.

How does this work differ from other uses of the word "group"?

The term "Group" is used in other ways that may or may not align with the use of the word "Group" in this document.

Relation to FOAF (Friend Of A Friend) "Group"

The [[[FOAF]]] is one of the earliest vocabularies to define a concept of a "Group", alongside other social concepts. In particular, relations like "member" and "membershipClass" can be used to describe who is a member of the group. FOAF defines "Group" like so:

The Group class represents a collection of individual agents (and may itself play the role of a Agent, ie. something that can perform actions).

This concept is intentionally quite broad, covering informal and ad-hoc groups, long-lived communities, organizational groups within a workplace, etc. [...]

Relation to vCard "Group"

The [[[RFC6350]]] defines a KIND of "group" like so:

a vCard representing a group of persons or entities. The group's member entities can be other vCards or other types of entities, such as email addresses or web sites. A group vCard will usually contain MEMBER properties to specify the members of the group, but it is not required to. A group vCard without MEMBER properties can be considered an abstract grouping, or one whose members are known empirically (perhaps "IETF Participants" or "Republican U.S. Senators").

All properties in a group vCard apply to the group as a whole, and not to any particular MEMBER. For example, an EMAIL property might specify the address of a mailing list associated with the group, and an IMPP property might refer to a group chat room.

The concept of membership, while optional, is one of the defining features of a group according to vCard. It is possible to describe membership in a group using both the vocabulary of vCard and the vocabulary defined in this document.

Relation to Activity Streams 2.0 "Group"

The [[[activitystreams-vocabulary]]] defines a Group only in the most general terms:

Represents a formal or informal collective of Actors.

The groups described by this document can be said to be instances of https://www.w3.org/ns/activitystreams#Group, but they carry additional considerations. Activity Streams 2.0 does not define a concept of membership within a "Group".

Note that the membership concepts defined in this document can be used with other types, and the protocols for managing membership depend on the presence of properties rather than the presence of types.

Relation to ActivityPub and "followers"

[[[activitypub]]] defines an actor system where activities can be published to outboxes and pushed to addressed audiences as notifications. Primarily, ActivityPub defines a way to manage "followers" via the "Follow" activity, which one can later "Accept" or "Reject":

The follow activity generally is a request to see the objects an actor creates. This makes the Followers collection an appropriate default target for delivery of notifications.

However, binding all social contexts to the concept of "followers" limits the range of social contexts that people can participate in. Being a follower does not carry any inherent social considerations beyond a simple desire to see more activities from a certain actor. Also, the concept of members is distinct from the concept of followers; it is possible to be a member without receiving activities intended for followers.

Relation to FEP-1b12 "Group"

[[[FEP-1b12]]] uses the Activity Streams 2.0 "Group" term directly, but assumes that any or all "Group" objects necessarily adhere to the protocol requirements laid out in the FEP. In FEP-1b12, a "Group" receives activities in its inbox, and publishes activities to its ActivityPub followers where the type is Announce and the object of the Announce is the received activity (instead of forwarding from inbox as defined in ActivityPub).

The groups described in this document are not constrained in which activities they produce. It is possible to produce Announce activities in accordance with the aforementioned FEP, but this is not a requirement of this document.

Relation to MLS "Group"

[[[RFC9420]]] and [[[RFC9750]]] define a notion of a "group" as a shared cryptographic state:

A client that is part of a group is a member of that group. As groups change membership and group or member properties, they advance from one epoch to another and the cryptographic state of the group evolves.

This document's notion of groups similarly allows for binding state to the group context, but without a strong requirement for cryptography or encryption.

Relation to similar concepts on other platforms

Some platforms implement "group" functionality, although it may not be called a "group".

  • Some social networking services have "groups" which users can join or leave as members, and a single context for posts known as a "wall".
  • Some social communication platforms have "guilds" which contain members and roles and invites, and allow members to post in multiple contexts called "channels" within the guild. (In some cases, the user-facing term for this functionality is "servers"; although at a service level these guilds may be hosted on the same server, they can be thought of as virtual servers.)
  • Various messaging platforms have "rooms", which maintain a list of members that pass messages to each other.

How does this differ from other Social CG task forces?

Relation to Forums and Threaded Discussion Task Force

A popular conception of "groups" is as a sort of forum. However, while the Forums task force deals with the organization of posts into threads and/or forums, the Groups task force deals with the organization of actors into groups which have members and roles. Thus, the work of these two task forces is complementary.

Relation to End-to-End Encryption (E2EE) Task Force

Since MLS is designed around a concept of "groups", alignment between this document's model of groups and the MLS model of groups can allow implementation of the group architecture via an MLS group's application data, and opening the possibility of both authenticated and encrypted groups.

Binding data to a group context

Information about a group

A group can be described like any other resource. The following terms from the ActivityStreams context (`https://www.w3.org/ns/activitystreams`) can be broadly useful:

`name` (`https://www.w3.org/ns/activitystreams#name`)
`summary` (`https://www.w3.org/ns/activitystreams#summary`)
The group can be rendered as text using these natural language properties.
`icon` (`https://www.w3.org/ns/activitystreams#icon`)
`image` (`https://www.w3.org/ns/activitystreams#image`)
The group can be rendered with visual elements using these properties.
`outbox` (`https://www.w3.org/ns/activitystreams#outbox`)
`streams` (`https://www.w3.org/ns/activitystreams#streams`)
The group can publish streams of activities here.
`inbox` (`http://www.w3.org/ns/ldp#inbox`)
The group can receive notifications and activities here.

TODO: any missing properties?

Tracking membership within a group

TODO: describe in more detail

A `members` collection contains `Member` records for each group member.

To manage your membership in a group, see Section .

Establishing roles within a group

TODO: needs discussion

For convenience, group managers can define roles to create subgroupings of group members

Permissions within a group

A group SHOULD publish information about which interactions it will accept, and from whom, by carrying an interaction policy in the `interactionPolicy` property of its actor object. Each entry names an interaction and states who the group will accept it from. The policy is published as a hint: it SHOULD describe what the group accepts, while deciding stays with the group actor itself (see ). An interaction is any action a policy names, whether it adds content to the group, acts on content already there, or acts on the group itself.

Interaction policies are already in use on individual posts rather than on groups, described in [[[gotosocial-interaction-controls]]], used by [[[FEP-044f]]] to obtain consent for quoting, and implemented on that basis by [[[mastodon-activitypub]]]. What this section adds is the group as a subject; the shape is unchanged.

Expressing an interaction policy

Each key of `interactionPolicy` names an interaction, and MUST map to an object with the properties `automaticApproval` and `manualApproval`. Each property MUST hold ActivityPub identifiers of actors or collections of actors, or the value `as:Public`. Values are identifiers rather than names (e.g. `https://group.example/g/cooks/followers` instead of `followers`). Full IRIs SHOULD be used, since many consumers compare values as plain strings without JSON-LD processing.

						{
						"type": "Group",
						"id": "https://group.example/g/cooks",
						"followers": "https://group.example/g/cooks/followers",
						"attributedTo": "https://group.example/g/cooks/moderators",
						"interactionPolicy": {
						"canJoin": {
						"automaticApproval": ["https://group.example/g/cooks/followers"],
						"manualApproval": ["https://www.w3.org/ns/activitystreams#Public"]
						}
						}
						}
					
A group whose followers may join without asking, while anyone else may ask. An actor in neither list, were there one, might expect refusal. The interaction name is illustrative only: this specification defines the shape, and the names are proposed separately.

An interaction that is absent or `null` is unconstrained by this policy.

Absence is deliberately not a denial. Publishing a policy is best practice rather than a requirement, so a group that publishes none, or omits an interaction from one, keeps federating exactly as groups do today.

A group states that an interaction is not allowed by anyone by naming nobody but itself: `automaticApproval` holding at most the group's own actor, and `manualApproval` empty or absent. This is not the same as leaving the interaction out. An entry naming nobody says the group enforces the interaction and permits no one else, while an absent entry says nothing at all.

						{
						"interactionPolicy": {
						"canJoin": {
						"automaticApproval": ["https://group.example/g/cooks"],
						"manualApproval": []
						}
						}
						}
					
A closed group. Only the group's own actor is named, so no one else may join and none may ask. Compare with omitting `canJoin` entirely, which tells a consumer nothing about joining.

This reads the lists as complete, which an open-world reading of the data would not: nothing else being named does not, in RDF terms, mean no one else is permitted. The closed reading is deliberate: a policy is the group's own statement of its own rules, so what it lists is meant as the whole list rather than inferred from absence.

A policy names actors and collections, so a group needs no vocabulary of roles to express one. A group that wants a grouping such as "safety team" or "trusted posters" publishes that collection and names it in the entries it should govern. A consumer can then check whether an actor is in the collection without knowing what the group means by the name.

Where a policy applies

A policy on the group's actor object governs interactions with the group itself and with the objects it has announced or authored. A policy on an individual object governs interactions with that object, and MUST take precedence over the group's where both name the same interaction.

						{
						"type": "Group",
						"id": "https://group.example/g/cooks",
						"interactionPolicy": {
						"canReply": {
						"automaticApproval": ["https://group.example/g/cooks/members"]
						}
						}
						}
					
						{
						"type": "Note",
						"id": "https://group.example/g/cooks/notes/rules",
						"attributedTo": "https://group.example/g/cooks",
						"interactionPolicy": {
						"canReply": {
						"automaticApproval": ["https://group.example/g/cooks/moderators"]
						}
						}
						}
					
Members may reply to anything the group carries, but this note (in this case authored by the group itself) allows replies from its moderators only. The note's own `canReply` takes precedence, so a member's reply to the rules is refused, while their replies elsewhere in the group are accepted.

An object that merely names a group in its `audience` asserts only its own author's claim, which does not by itself bring the object under the group's policy. A consumer MUST NOT treat such a claim as evidence that the object belongs to the group.

A policy governs who may act, not who may see. Naming an actor in one MUST NOT be read as granting it visibility of the group or of any object within the group, and an actor that cannot see an object MUST NOT be permitted to interact with it. Visibility remains determined by `to`, `cc` and `audience`.

						{
						"type": "Note",
						"id": "https://alice.example/notes/1",
						"attributedTo": "https://alice.example",
						"audience": "https://group.example/g/cooks",
						"interactionPolicy": {
						"canReply": {
						"automaticApproval": ["https://group.example/g/cooks/members"]
						}
						}
						}
					
A note posted to the group, carrying its own policy for replies to it. Where the group's policy also names `canReply`, the note's own policy governs replies to this note.

Types of interactions

The interactions themselves, and their names, are not defined here. See issue #26.

Interaction names are IRIs, so a specification or an implementation MAY define its own without coordination, provided it does so in a namespace it controls.

						{
						"interactionPolicy": {
						"canJoin": {
						"automaticApproval": ["https://www.w3.org/ns/activitystreams#Public"]
						},
						"https://software.example/ns#canPin": {
						"automaticApproval": ["https://group.example/g/cooks/moderators"]
						}
						}
						}
					
An implementation-defined interaction alongside a shared one, named in a namespace its definer controls. A consumer that does not recognise `canPin` still applies `canJoin`.

Publishing a policy

A group SHOULD serve an interaction policy with its actor object that reflects what it actually enforces. The policy it serves can be the rules its own server evaluates when an activity arrives, or a description of rules it holds separately.

Including an interaction in its policy indicates two things: that the group supports interaction policies, and that it enforces that interaction.

A group SHOULD therefore include every interaction it enforces, including those whose value does not differ from the default.

Silence is ambiguous in a way that matters: a group stating nothing might place no restrictions, or might run software that has not implemented interaction policies. The two call for opposite handling, since there is no use in waiting on a decision from a server that will never make one. Writing out a permission, even at its default value, settles it.

Reading a policy

To anyone other than the group publishing it, a policy is a declaration of intent. A client that understands one SHOULD use it to present only the actions a user is likely to be permitted to perform, and an actor SHOULD use it to decide whether an attempt is worth making.

An actor interacting with a remote group MUST NOT treat a permissive policy as a guarantee that the interaction will be accepted, and SHOULD NOT read an absent interaction as a denial.

A consumer that does not recognise an interaction SHOULD continue to apply the interactions it does recognise, rather than disregarding the policy as a whole.

Where a group publishes no policy at all, a consumer cannot tell whether it places no restrictions or does not support interaction policies.

Where a group publishes a policy that omits a given interaction, the group supports interaction policies but does not describe that interaction as restricted.

In either case an actor can still attempt the interaction, as it would with any group that publishes no policy. Since the group has advertised no support for it, a consumer MUST NOT rely on it being recognised or accepted, and SHOULD NOT present it as having taken effect within the group until the group acts on it.

Attempting an action

An actor attempts an interaction by sending the activity to the group, usually by delivering it to the group's inbox. There is no separate step of asking first: the activity is the request, in either case.

Whilst interaction policies on posts outside any group have a separate step (because there is no group to redistribute: the interacting actor distributes its own interaction, so it must obtain approval beforehand and attach it), a group instead distributes the interaction itself, on the interacting actor's behalf and under its own authority, which both performs the interaction and evidences that it was permitted. It also saves the round trips, since a group can reject an interaction simply by not distributing it.

What the policy tells the actor's client is how to describe the state while it waits. Presenting the interaction as pending is always correct, in either case, and a client MAY do only that. Where the actor is listed in `automaticApproval` a client MAY instead present it as having taken effect; where the actor is listed in `manualApproval` it SHOULD NOT, since a decision is pending and may never come.

Either presentation is provisional. The actual state follows from what the group does: an `Accept`, or the group acting on the interaction and publishing the result to its outbox.

This is how a `Follow` of an actor with `manuallyApprovesFollowers` already behaves. The same activity is sent either way, and the difference the user sees is "Following" rather than "Requested".

						{
						"type": "Join",
						"id": "https://alice.example/activities/1",
						"actor": "https://alice.example",
						"object": "https://group.example/g/cooks",
						"to": "https://group.example/g/cooks"
						}
					
Alice asks to join by sending a `Join` to the group. The same activity is sent whether the policy lists her under `automaticApproval` or `manualApproval`.

Enforcing a policy

A group server MUST check each activity delivered to its inbox against its own rules, and MUST NOT rely on a sender having consulted or followed its policy.

A group SHOULD accept an interaction from an actor listed in `automaticApproval`, MAY hold one from an actor listed in `manualApproval` pending a decision, and SHOULD NOT accept one from an actor listed in neither.

An interaction takes effect within the group only once the group acts on it, by announcing it, or by acting on what it affects and publishing the outcome to its outbox. That action is also what evidences permission: a group that redistributes an interaction thereby asserts that it permitted it, so a separate approval object is not needed.

						{
						"type": "Announce",
						"id": "https://group.example/g/cooks/activities/3",
						"actor": "https://group.example/g/cooks",
						"object": {
						"type": "Note",
						"id": "https://alice.example/notes/1",
						"attributedTo": "https://alice.example",
						"audience": "https://group.example/g/cooks"
						},
						"to": ["https://group.example/g/cooks/followers"]
						}
					
The group announcing Alice's note both carries out the interaction and evidences that the group permitted it. An `Accept` may also be sent to Alice, but the announcement is what puts the note in the group.

Because a group can have more than one actor able to decide on its behalf, and those actors can be on other servers, a policy MAY also state who can accept/reject interactions. Where it does, a group server may reject any decision from an actor it does not name.

						{
						"interactionPolicy": {
						"canJoin": {
						"manualApproval": ["https://www.w3.org/ns/activitystreams#Public"]
						},
						"canAcceptJoin": {
						"automaticApproval": ["https://group.example/g/cooks/greeters"]
						}
						}
						}
					
Anyone may ask to join, and someone in this group's "greeters" collection decides. The deciding actor can be a member of any collection(s) the group names, and need not be its "moderators" or "admins". Names are only illustrative.
						{
						"type": "Accept",
						"id": "https://bob.example/activities/7",
						"actor": "https://bob.example",
						"object": "https://alice.example/activities/1",
						"to": "https://group.example/g/cooks"
						}
					
Bob, a greeter the group's server does not host, accepts Alice's `Join` (see Attempting an action) by sending an `Accept` activity to the group's inbox. The group checks Bob against the collection its policy names. Bob's `Accept` is a decision, not the outcome.
						{
						"type": "Accept",
						"id": "https://group.example/g/cooks/activities/8",
						"actor": "https://group.example/g/cooks",
						"object": "https://alice.example/activities/1",
						"to": "https://alice.example"
						}
					
The group then sends its own `Accept` to Alice, once it has acted on Bob's decision and added her as a member. Alice's server takes the outcome from this activity, whose actor is the group, and not from Bob's.

Scoping content within a group

TODO: needs discussion

`context` or something similar?

Your membership within a group

Join a group

TODO: needs discussion

  1. You create a membership record
  2. The group then activates your membership
  3. Confirm your membership by issuing a Join activity within the group context

See for rationale.

Leave a group

TODO: needs discussion

  1. Issue a Leave activity within the group?
  2. The group deactivates and/or deletes your membership

Terms defined

Member

URI
https://swicg.github.io/groups/#Member
Label
membership record
Comment
Tracks membership-related state within a group context.
See also
org:Membership

members

URI
https://swicg.github.io/groups/#members
Label
has a group members collection
Comment
Contains membership records for all members in the group, both inactive and active.
Domain
Group
Range
Collection
See also
foaf:member
vcard:hasMember
org:hasMember

memberships

URI
https://swicg.github.io/groups/#memberships
Label
has a memberships collection
Comment
Contains memberships records which refer to you. When you join a group, its corresponding membership will be added to this collection. When you leave a group, its corresponding membership can be deleted and/or removed from this collection.
Range
Collection
See also
org:memberOf
org:hasMembership

User stories

Groups

For information about the group:

Membership

For voluntarily managing one's own membership:

For managing other people's memberships in a group you are authorized to manage:

Roles

Permissions

Invites

Posts

Structure

Unsorted user stories

Potential approaches

Define side effects for the Join activity similarly to ActivityPub's Follow activity

ActivityPub defines a flow for Follow, Accept, Reject, and Undo activities with the following states:

NONE
No follower relation exists. To establish one, a new Follow request must be sent.
PENDING
A Follow activity has been sent, but the Follow activity is not the object of any future Accept, Reject, or Undo activity.
FOLLOWER
An Accept Follow exists, and the Accept is not the object of any future Undo activity, and the Follow is not the object of any future Reject or Undo activity.

And the following state transitions:

This design from ActivityPub has the following shortcomings and issues:

It can seem appealing to mirror the Follow activity flow and its side effects, but with the Join activity instead of the Follow activity -- perhaps assuming that familiarity with the existing Follow flow will make it easier to implement a parallel Join flow. However, doing so inherits all of the aforementioned issues and would lead to a repeat of the implementation errors faced in practice.

A more robust approach with ACID compliance

To address the shortcomings of the mirrored flow described above, we need a design with the following properties:

Membership records

The simplest way to create a membership record is to rely on something like HTTP PUT at an indexed location:

					PUT /members/alice.example HTTP/1.1
					Content-Type: application/activity+json

					{
						"type": "_:Member",
						"_:user": {"id": "https://alice.example"},
						"name": "arisu"
					}
					
PUT a membership record into the group's members collection for alice.example. Alice's display name in the group is set to "arisu", overriding Alice's default name (which is not stated here).

Alternatively, HTTP POST can be used instead, to let the members collection choose the resulting Location:

					POST /members/ HTTP/1.1
					Content-Type: application/activity+json

					{
						"type": "_:Member",
						"_:user": {"id": "https://alice.example"},
						"name": "arisu"
					}
					
POST to the group's members collection for alice.example. Alice's display name in the group is set to "arisu", overriding Alice's default name (which is not stated here).

The response might show that the membership is pending:

					HTTP/1.1 201 Created
					Location: https://group.example/members/01KSQFYKVAGWH0HW09QMTD2VC9
					Content-Type: application/activity+json

					{
						"type": "_:Member",
						"_:user": {"id": "https://alice.example"},
						"_:pending": true,
						"name": "arisu",
					}
					
The membership record has been created.

In this case, the member and the user are not the same -- the member wraps the user. Similarly, metadata about your membership within a group is not metadata of the actor. We can use membership records/entities to achieve this separation.

Using Join to describe actually joining

In MLS, clients are added to groups via Proposal/Commit, and then separately receive a Welcome message that contains the keying material needed to actually join the group. This "add before join" pattern means that the Join does not exist before the client has actually joined the group.

Per MLS Architecture Section 3.7 "Users, Clients, and Groups":

Note that until a client has been added to the group and contributed to the group secret in a manner verifiable by other members of the group, other members cannot assume that the client is a member of the group; for instance, the newly added member might not have received the Welcome message or been unable to decrypt it for some reason.

We could have the new member confirm their membership with a Join activity (which gets republished to the group's outbox), instead of using the Join activity to manage the membership itself.

Changelog

Initial CG Draft