Vibefed kb-fediverse-group-federation

install
source · Clone the upstream repo
git clone https://github.com/reiver/vibefed
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/reiver/vibefed "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/kb-fediverse-group-federation" ~/.claude/skills/reiver-vibefed-kb-fediverse-group-federation && rm -rf "$T"
manifest: skills/kb-fediverse-group-federation/SKILL.md
source content

Fediverse Group Federation (FEP-1b12) — Complete Reference

Overview

Internet forums — communities where users submit posts and discuss topics — are implemented in ActivityPub using

Group
actors. FEP-1b12 (authored by Felix Ableitner / nutomic, received 2022-11-12, status FINAL since 2023-02-09) codifies the common patterns already used in production by Lemmy, Friendica, Hubzilla, Lotide, and PeerTube.

The core mechanism: a

Group
actor receives activities from users, validates them, wraps them in
Announce
activities, and distributes them to all followers. The
audience
property on objects identifies which group they belong to.


1. The Group Actor

Each forum/community is represented by a

Group
actor with standard ActivityPub endpoints:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Group",
  "id": "https://lemmy.example/c/technology",
  "preferredUsername": "technology",
  "name": "Technology",
  "inbox": "https://lemmy.example/c/technology/inbox",
  "outbox": "https://lemmy.example/c/technology/outbox",
  "followers": "https://lemmy.example/c/technology/followers",
  "attributedTo": "https://lemmy.example/c/technology/moderators"
}

The Group actor operates as an automated actor:

  • It accepts
    Follow
    requests automatically (for public groups)
  • It validates incoming activities and redistributes them
  • It is discovered via WebFinger like any other actor

Following a Group

Users follow a Group using the standard Follow/Accept handshake:

  1. User sends
    Follow
    to the group's inbox
  2. Group auto-accepts with an
    Accept{Follow}
    (public groups SHOULD do this)
  3. User is added to the group's
    followers
    collection
  4. From now on, the group distributes content to that user's inbox

Unfollowing uses

Undo{Follow}
, same as unfollowing a person.


2. The
audience
Property

The

audience
property (defined in ActivityStreams 2.0 but previously unused in the wild) identifies which group an object belongs to:

{
  "type": "Page",
  "id": "https://lemmy.example/post/123",
  "attributedTo": "https://alice.example/users/alice",
  "name": "Check out this new technology",
  "audience": "https://lemmy.example/c/technology"
}

Why
audience
instead of
to
or
attributedTo

Before FEP-1b12, implementations placed the group identifier in various fields:

  • to
    /
    cc
    — requires URL resolution to determine if a recipient is a Group actor; mixed with other addressees
  • attributedTo
    — conflicts with the actual author of the post

audience
solves this cleanly:

  • No extension needed (it's already in the ActivityStreams vocabulary)
  • Recipients can instantly identify the group without resolving URLs
  • Existing implementations that don't understand groups simply ignore it
  • Doesn't conflict with delivery addressing (
    to
    /
    cc
    ) or authorship (
    attributedTo
    )

All objects and activities related to a group SHOULD include

audience
pointing to the group's id.


3. Threads and Comments

Top-level posts

Top-level posts (threads) are typically

Page
objects with a
name
property serving as the title:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Page",
  "id": "https://lemmy.example/post/123",
  "attributedTo": "https://alice.example/users/alice",
  "to": ["https://www.w3.org/ns/activitystreams#Public"],
  "cc": ["https://lemmy.example/c/technology/followers"],
  "name": "Check out this new technology",
  "content": "<p>I found something interesting...</p>",
  "audience": "https://lemmy.example/c/technology"
}

The

Page
type is exemplary — implementations may use other types. The key distinction is that top-level posts have a
name
(title) while replies typically do not.

Replies (comments)

Replies are

Note
objects with
inReplyTo
linking to the parent:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Note",
  "id": "https://bob.example/comment/456",
  "attributedTo": "https://bob.example/users/bob",
  "to": ["https://www.w3.org/ns/activitystreams#Public"],
  "cc": ["https://lemmy.example/c/technology/followers"],
  "inReplyTo": "https://lemmy.example/post/123",
  "content": "<p>Great find! Here is what I think...</p>",
  "audience": "https://lemmy.example/c/technology"
}

inReplyTo
can point to the top-level post (for direct replies) or to another comment (for nested replies). Lemmy displays comments in a tree structure.

replies
collection

Groups MAY maintain a

replies
collection on the group or on individual posts, enabling navigation of the thread hierarchy.


4. The Announce Wrapping Mechanism

This is the core of FEP-1b12 group federation. When a user submits content to a group, the group wraps it in an

Announce
and distributes it to all followers.

Flow

  1. Alice creates a post and sends
    Create{Page}
    to the group's inbox
  2. The group validates the activity (checks blocks, permissions)
  3. The group wraps it:
    Announce{Create{Page}}
  4. The group sends the
    Announce
    to all follower inboxes

What the Announce looks like

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Announce",
  "id": "https://lemmy.example/activities/announce/789",
  "actor": "https://lemmy.example/c/technology",
  "to": ["https://www.w3.org/ns/activitystreams#Public"],
  "cc": ["https://lemmy.example/c/technology/followers"],
  "object": {
    "type": "Create",
    "id": "https://alice.example/activities/create/123",
    "actor": "https://alice.example/users/alice",
    "object": {
      "type": "Page",
      "id": "https://lemmy.example/post/123",
      "attributedTo": "https://alice.example/users/alice",
      "name": "Check out this new technology",
      "audience": "https://lemmy.example/c/technology"
    }
  }
}

What the Announce means

The

Announce
serves as proof that the group approved the content. Recipients can trust that the group validated the activity. After processing, recipients may optionally discard the
Announce
wrapper and work with the inner activity directly.

This mechanism supports any activity type — not just

Create
. Comments, updates, and moderation actions all flow through the same
Announce
wrapping.

Validation before Announcing

Groups SHOULD validate incoming activities before wrapping and distributing:

  • Check the sending actor/domain is not blocked
  • Optionally require the sender to be a follower
  • Optionally require moderator approval
  • Check the
    audience
    property matches this group
  • Groups MUST NOT forward activities that don't include the group in
    audience

5. Announce(Activity) vs Announce(Object)

FEP-1b12 specifies Announce(Activity) — wrapping the full

Create
activity inside the Announce. However, several major implementations send Announce(Object) — wrapping just the object directly.

ImplementationSendsAccepts
LemmyAnnounce(Activity) + Announce(Object) for Mastodon compatBoth
FriendicaAnnounce(Object)Both
HubzillaAnnounce(Object)Both
NodeBBAnnounce(Object)Both
DiscourseBoth (added Object support for NodeBB compat)

The FEP author (nutomic) acknowledged not knowing about Announce(Object) usage when writing the spec. Community consensus is that

Announce(Object)
should be treated as shorthand for
Announce(Create(Object))
.

Recommendation for implementors: When sending, Announce(Activity) is spec-compliant. When receiving, accept both forms — treat Announce(Object) as equivalent to Announce(Create(Object)). The FEP's FINAL status makes formal amendment difficult, but real-world interoperability requires handling both.


6. Group Moderation

Group moderation is an optional feature defined in FEP-1b12, initially implemented only in Lemmy.

Moderator list

Moderators are listed in the group's

attributedTo
property, which resolves to a collection of actor URLs:

{
  "type": "Group",
  "id": "https://lemmy.example/c/technology",
  "attributedTo": "https://lemmy.example/c/technology/moderators"
}

The moderators collection:

{
  "type": "OrderedCollection",
  "id": "https://lemmy.example/c/technology/moderators",
  "orderedItems": [
    "https://alice.example/users/alice",
    "https://bob.example/users/bob"
  ]
}

Adding and removing moderators

Moderator changes use

Add
and
Remove
activities:

{
  "type": "Add",
  "actor": "https://alice.example/users/alice",
  "object": "https://charlie.example/users/charlie",
  "target": "https://lemmy.example/c/technology/moderators"
}

The actor performing the Add/Remove MUST already appear in the

attributedTo
collection (i.e., must already be a moderator).

Content moderation

Moderators remove content using

Remove
activities (not
Delete
— the ActivityPub spec implies
Delete
should only come from the object's author):

{
  "type": "Remove",
  "actor": "https://alice.example/users/alice",
  "object": "https://lemmy.example/post/123",
  "target": "https://lemmy.example/c/technology"
}

Distribution of moderation actions

Moderation activities MUST also be wrapped in

Announce
by the group and distributed to all followers, so moderation takes effect across all federated instances.

Server administrators can also send moderation activities. The group must wrap these in

Announce
for distribution.


7. Interoperability with Non-Group-Aware Software

A key design goal of FEP-1b12 is backward compatibility with software that doesn't understand groups (e.g., Mastodon).

How it works

When a group sends

Announce{Create{Note}}
to a Mastodon user who follows the group, Mastodon sees it as a regular boost/reblog. The user sees the content in their timeline attributed to the original author, with the group shown as the booster. This is imperfect but functional.

Known issues

  • Mastodon reply routing: Mastodon sends replies only to mentioned actors, not to the group. The group never receives the reply and cannot redistribute it to other followers. This breaks cross-instance comment threading.
  • Vote/Like counts: Likes sent to the post author's inbox are processed locally but not announced by the group to followers, causing count discrepancies across instances.
  • Hubzilla workaround: Hubzilla historically made the forum owner appear as the post author since "Mastodon will reject or ignore any messages from people you don't follow."
  • Mastodon's own groups: Mastodon's pending groups PR (#19059) uses FEP-400e instead, which is incompatible with FEP-1b12. A Mastodon developer (Claire) argued that FEP-1b12's compatibility with non-group-aware software is actually a problem because it prevents enforcing group-specific interaction semantics (like preventing reblogging of group posts).

8. FEP-1b12 vs FEP-400e (Publicly Appendable Collections)

Two incompatible approaches to groups exist in the fediverse:

FEP-1b12FEP-400e
MechanismGroup wraps activities in
Announce
Actors send
Add
to group's
wall
collection
Group identification
audience
property
wall
property on Group actor
Content distributionGroup sends
Announce
to followers
Group sends
Add
to followers
DetectionCheck for
Group
type +
audience
usage
Check for
wall
property on actor
StatusFINAL
AdoptionLemmy, Friendica, Hubzilla, Lotide, PeerTube, Kbin, NodeBBSmithereen (~25 MAU), Mastodon (pending PR)

FEP-1b12 is the de-facto standard used by the vast majority of group implementations. FEP-400e is currently only implemented by Smithereen and proposed for Mastodon.

There is no way to detect which approach a

Group
actor uses from the actor document alone without checking for the presence of a
wall
property. trwnh noted there is "nothing actually special about the Group type" in ActivityStreams 2.0 — the behavior must be inferred.


9. Implementation Matrix

PlatformGroup TypeaudienceAnnounce wrappingModerationNotes
LemmyCommunity =
Group
Yes (since 0.17.0)Announce(Activity) + Announce(Object)Full (attributedTo)Reference implementation
FriendicaForum =
Group
YesAnnounce(Object)YesFederated forums since 2019.03
HubzillaForum =
Group
YesAnnounce(Object)YesWorkaround for Mastodon compat
LotideCommunity =
Group
YesYes
PeerTubeChannel =
Group
YesYesVideo-oriented
Kbin/MbinMagazine =
Group
YesYesYesLemmy-compatible
NodeBBCategory =
Group
Yes (deviation)Announce(Object)Categories only;
audience
at 2nd-order parent
GuppeAuto-group =
Group
NoAnnounce(Object)No~200 lines; simplest possible group (boost bot)
MastodonPendingNoNo (uses FEP-400e)PR #19059; incompatible with FEP-1b12

10. Creating a Post in a Group — Complete Flow

This section traces the full lifecycle of a group post.

Step 1: User creates a post

Alice writes a post for the Technology community and sends it to the group's inbox:

{
  "type": "Create",
  "actor": "https://alice.example/users/alice",
  "to": ["https://www.w3.org/ns/activitystreams#Public"],
  "cc": ["https://lemmy.example/c/technology"],
  "object": {
    "type": "Page",
    "attributedTo": "https://alice.example/users/alice",
    "name": "Check out this new technology",
    "content": "<p>I found something interesting...</p>",
    "audience": "https://lemmy.example/c/technology"
  }
}

Step 2: Group validates

The group checks:

  • Alice is not blocked
  • The domain is not blocked
  • audience
    matches this group
  • (Optional) Alice is a follower
  • (Optional) Moderator approval required

Step 3: Group announces

The group wraps and distributes:

{
  "type": "Announce",
  "actor": "https://lemmy.example/c/technology",
  "to": ["https://www.w3.org/ns/activitystreams#Public"],
  "cc": ["https://lemmy.example/c/technology/followers"],
  "object": {
    "type": "Create",
    "actor": "https://alice.example/users/alice",
    "object": { ... }
  }
}

Step 4: Followers receive

Bob (on a different server) receives the

Announce
in his inbox. His server:

  1. Verifies the
    Announce
    came from the group (HTTP Signature check)
  2. Extracts the inner
    Create{Page}
  3. Displays the post in the Technology community feed
  4. The
    Announce
    wrapper serves as proof the group approved this content

Step 5: Bob replies

Bob sends a reply (

Create{Note}
with
inReplyTo
) to the group's inbox. The group validates and announces it to all followers.


11. Discovery

Groups are discoverable via WebFinger. Lemmy uses the

!
prefix convention for communities:

GET /.well-known/webfinger?resource=acct:technology@lemmy.example

The response includes a

self
link with
type: Group
in properties:

{
  "rel": "self",
  "type": "application/activity+json",
  "href": "https://lemmy.example/c/technology",
  "properties": {
    "https://www.w3.org/ns/activitystreams#type": "Group"
  }
}

The

!
prefix is a Lemmy UI convention, not a protocol requirement. NodeBB uses
@
prefix like regular actors. The WebFinger response's
type: Group
property is the canonical way to detect a group.


12. Known Issues and Pitfalls

  • Mastodon reply routing: Mastodon doesn't include the group in replies, so the group can't redistribute them. This is the biggest cross-platform interop issue.
  • Announce(Object) non-compliance: Hubzilla, Friendica, and NodeBB send Announce(Object) which is technically non-compliant. Consumers must accept both forms.
  • Vote count discrepancies: Likes/dislikes sent to post author instead of group are not redistributed, causing different counts on different instances.
  • No
    <h1>
    in Page titles
    : The title goes in
    name
    , not in HTML content.
  • Delete vs Remove: Use
    Remove
    for moderation actions, not
    Delete
    .
    Delete
    implies the actor is the author of the object.
  • Private groups: FEP-1b12 explicitly removed private groups from scope. Lemmy and Friendica implement them independently by restricting activity visibility to followers only and verifying member lists.
  • Group detection: There is no reliable way to distinguish a FEP-1b12 group from a FEP-400e group from the actor document alone without checking for a
    wall
    property.