Vibefed kb-fediverse-following

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-following" ~/.claude/skills/reiver-vibefed-kb-fediverse-following && rm -rf "$T"
manifest: skills/kb-fediverse-following/SKILL.md
source content

Fediverse Following — Complete Reference

Overview

Following on the Fediverse is a formal ActivityPub handshake, not a simple database write. It involves activity delivery between servers, an explicit approval step, and ongoing delivery of content. Understanding all the steps — and what can go wrong — is essential for correct implementation.


1. The Actor Object

Before any following can happen, each user is represented as an actor — a JSON-LD object served at a stable URL. The actor contains the endpoints and collections needed for the follow flow:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Person",
  "id": "https://example.com/users/alice",
  "preferredUsername": "alice",
  "inbox": "https://example.com/users/alice/inbox",
  "outbox": "https://example.com/users/alice/outbox",
  "followers": "https://example.com/users/alice/followers",
  "following": "https://example.com/users/alice/following",
  "manuallyApprovesFollowers": false
}

Key properties for following:

  • inbox
    — the URL where activities are POSTed to reach this actor
  • followers
    — an
    OrderedCollection
    of actors who follow this actor
  • following
    — an
    OrderedCollection
    of actors this actor follows
  • manuallyApprovesFollowers
    false
    = open/unlocked account (auto-accepts);
    true
    = locked account (requires manual approval). Defined in the ActivityPub Miscellaneous Terms as
    as:manuallyApprovesFollowers
    .

2. The Follow Handshake — Step by Step

Following is a four-message exchange in the general case. For unlocked accounts it collapses to three (the Accept is sent automatically).

Step 1: Alice sends a
Follow
activity

Alice's server POSTs a

Follow
activity to Bob's inbox:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://alice.example/activities/follow-1",
  "type": "Follow",
  "actor": "https://alice.example/users/alice",
  "object": "https://bob.example/users/bob"
}
  • The
    object
    is the actor being followed (Bob)
  • The
    actor
    is the one initiating the follow (Alice)
  • The
    id
    should be a unique, dereferenceable URI on Alice's server (though in practice many implementations use non-dereferenceable UUIDs)
  • At this point, Alice's server may add Bob to Alice's
    following
    collection tentatively, or wait until the
    Accept
    arrives — implementations differ

Step 2: Bob's server receives the
Follow

On receiving the

Follow
in Bob's inbox, Bob's server:

  1. Validates the HTTP Signature on the request (to confirm it came from Alice's server)
  2. Checks whether Bob has
    manuallyApprovesFollowers: true
    or
    false
  3. Either queues a follow request for Bob to approve (locked account), or automatically proceeds to send an
    Accept

At this stage the follow is pending — Alice is not yet in Bob's

followers
collection.

Step 3: Bob's server sends an
Accept
(or
Reject
)

For unlocked accounts

Accept
is sent automatically:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://bob.example/activities/accept-1",
  "type": "Accept",
  "actor": "https://bob.example/users/bob",
  "object": {
    "type": "Follow",
    "actor": "https://alice.example/users/alice",
    "object": "https://bob.example/users/bob"
  }
}

The

object
of the
Accept
is the original
Follow
activity (or just its
id
URI — either form is acceptable).

For locked accounts — Bob manually approves or denies. If denied, a

Reject
is sent with the same structure:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://bob.example/activities/reject-1",
  "type": "Reject",
  "actor": "https://bob.example/users/bob",
  "object": "https://alice.example/activities/follow-1"
}

Important spec note: Servers MAY choose to not send a

Reject
at all (e.g. to protect a user's privacy — not rejecting leaks the fact that the account exists). This means the follow request may be silently ignored, leaving Alice's server in an indefinite pending state. Implementations should handle this gracefully.

Step 4: Alice's server receives the
Accept

On receiving the

Accept
:

  • Alice's server adds Bob to Alice's
    following
    collection
  • Bob's server adds Alice to Bob's
    followers
    collection
  • The follow relationship is now active

From this point on, when Bob creates a post addressed to his followers, his server will deliver it to Alice's inbox.


3. Unfollowing — The
Undo Follow

To unfollow, Alice sends an

Undo
activity wrapping the original
Follow
:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://alice.example/activities/undo-follow-1",
  "type": "Undo",
  "actor": "https://alice.example/users/alice",
  "object": {
    "type": "Follow",
    "actor": "https://alice.example/users/alice",
    "object": "https://bob.example/users/bob"
  }
}

This is POSTed to Bob's inbox. On receipt, Bob's server removes Alice from Bob's

followers
collection. Alice's server removes Bob from Alice's
following
collection.

The spec requires that the

Undo
and the activity being undone have the same actor. You cannot undo another actor's Follow.


4. The
followers
and
following
Collections

Both are

OrderedCollection
objects. They may be paginated with
OrderedCollectionPage
. The actor object contains their URLs; fetching those URLs with
Accept: application/activity+json
returns the collection.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://bob.example/users/bob/followers",
  "type": "OrderedCollection",
  "totalItems": 412,
  "first": "https://bob.example/users/bob/followers?page=1"
}

Implementations may choose to hide the actual members of these collections for privacy (returning

totalItems
but an empty
items
array, or omitting
first
/
next
page links). This is valid and common.


5. How Content is Delivered to Followers

When Bob creates a public post, his server must deliver it to all his followers. This is where the follow relationship does its work.

Addressing

A public post addressed to followers is typically addressed as:

{
  "to": ["https://www.w3.org/ns/activitystreams#Public"],
  "cc": ["https://bob.example/users/bob/followers"]
}

A followers-only post omits the public audience:

{
  "to": ["https://bob.example/users/bob/followers"],
  "cc": []
}

Delivery mechanics

Bob's server must POST the

Create
activity to the
inbox
of every actor in Bob's
followers
collection. For followers on many different servers, this can mean hundreds of HTTP POST requests.

Shared inbox optimisation

To avoid sending N requests to the same server (when multiple followers are on the same instance), the actor object may declare a

sharedInbox
:

{
  "endpoints": {
    "sharedInbox": "https://alice.example/inbox"
  }
}

When a

sharedInbox
is available, the sending server can POST once to that shared inbox instead of once per follower on that instance. The receiving server then fans out internally to all relevant local followers. Mastodon always provides a shared inbox. Using it is optional but strongly recommended for performance.


6. Visibility Levels and the
to
/
cc
Fields

The follow relationship determines what appears in a follower's Home timeline. The

to
and
cc
fields on activities determine visibility:

Visibility
to
cc
Public
as:Public
followers collection
Unlistedfollowers collection
as:Public
Followers-onlyfollowers collection(empty)
Direct (mention-only)mentioned actors(empty)

The special URI

https://www.w3.org/ns/activitystreams#Public
(often written as
as:Public
) signals that the post is publicly accessible.

Posts addressed to followers-only are only delivered to followers' inboxes, not crawlable publicly. A server receiving such a post should only show it to users who follow the author.


7. Locked Accounts and Pending Requests

When

manuallyApprovesFollowers
is
true
:

  • Incoming
    Follow
    activities are queued, not immediately accepted
  • The follower sees a "pending" state in the UI
  • The followed user sees a follow request notification
  • They can
    Accept
    (sending an
    Accept
    activity) or
    Reject
    (sending a
    Reject
    activity, or silently ignoring it)
  • Until accepted, the follower does NOT receive the account's posts

There is no standard way for a server to query whether a pending follow request has been accepted or rejected — implementations typically rely on eventually receiving an

Accept
or
Reject
, or timing out.


8. Account Migration — The
Move
Activity

ActivityPub does not natively support moving an account between servers, but a convention has emerged (led by Mastodon) using the

Move
activity and the
alsoKnownAs
/
movedTo
properties.

The process

  1. New account declares

    alsoKnownAs
    pointing to the old account:

    {
      "type": "Person",
      "id": "https://new.example/users/alice",
      "alsoKnownAs": ["https://old.example/users/alice"]
    }
    
  2. Old account sends a

    Move
    activity to all its followers:

    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Move",
      "actor": "https://old.example/users/alice",
      "object": "https://old.example/users/alice",
      "target": "https://new.example/users/alice"
    }
    
  3. Receiving servers (i.e. servers that have followers of the old account) automatically re-issue a

    Follow
    from each of those followers to the new account, effectively migrating the follower list.

Important limitations

  • Only followers are migrated — not posts, likes, bookmarks, or lists
  • The old account's posts remain on the old server (or disappear if the server is shut down)
  • Not all implementations support
    Move
    — GotoSocial notably lacked support for a time
  • The
    alsoKnownAs
    must be set on the new account before the
    Move
    is sent from the old account, or receiving servers may reject it

9. Common Implementation Mistakes

  • Not waiting for
    Accept
    before adding to
    following
    : Adding the followee to
    following
    before the
    Accept
    arrives means the collection is inaccurate for locked accounts
  • Not handling silent
    Reject
    : If a locked account never sends a
    Reject
    , the pending state must time out gracefully
  • Not validating HTTP Signatures on incoming
    Follow
    activities: anyone could POST a fake
    Follow
    to your inbox
  • Sending
    Accept
    to the wrong URL
    : The
    Accept
    must be delivered to the
    inbox
    of the Follow's
    actor
    , not the
    object
  • Not using
    sharedInbox
    : Sending one request per follower per instance instead of using
    sharedInbox
    causes serious performance problems at scale
  • Forgetting to handle
    Undo Follow
    : When Alice unfollows Bob, Bob's server must remove Alice from the
    followers
    collection, or Alice will keep receiving Bob's posts
  • Exposing full followers list without privacy consideration: Some users expect their followers/following lists to be private; implementations should support hiding collection members while still providing
    totalItems

10. Summary: The Minimum a Correct Implementation Must Do

DirectionActivityRequired action
Outgoing followSend
Follow
to target's
inbox
Add to local pending state
Incoming
Accept
Receive in own
inbox
Move from pending → active; add to
following
Incoming
Reject
(or silence)
Receive in own
inbox
(or timeout)
Remove from pending
Incoming
Follow
Receive in own
inbox
Queue or auto-accept; send
Accept
/
Reject
Outgoing
Accept
Send to follower's
inbox
Add follower to
followers
collection
Outgoing unfollowSend
Undo{Follow}
to target's
inbox
Remove from
following
Incoming
Undo Follow
Receive in own
inbox
Remove actor from
followers
collection
Delivering postsPOST
Create
to each follower's
inbox
(or
sharedInbox
)