docs/INTEROP.md collects five research passes from 2026-10-01: Mastodon 4.7 and GoToSocial 0.22, the Misskey and Pleroma families, the threadiverse (Lemmy 0.19/1.0, PieFed, Mbin, NodeBB), media and long-form (PeerTube, Loops, Pixelfed, WordPress, Ghost, events, audio, books, Threads, Flipboard, Bridgy Fed), and cross-cutting FEPs and signatures. Each claim was checked against source code or live fetches, with dates and versions. It is checked against our own code: what is already right, three cheap things that are wrong today (summary read as a CW on every type, unchecked usernames, an undefined context term), what the rich client needs kept (a post kind with typed payloads, raw capture and provenance for the details view), and the six privacy choices the owner has to make. The roadmap's P5 becomes P5 to P8, ordered by that evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
54 KiB
Interop: what every peer sends, what it expects, and what PrivaPub still drops
Research as of 2026-10-01: the platforms' source on their default branches, live ActivityPub fetches from large instances, release notes and the FEP repository. Version numbers are what was current that day. Claims the research could not confirm from a primary source are marked (unconfirmed). The sources are at the end.
This file is for two readers:
- Whoever changes federation code. Every gotcha below has broken somebody.
- Whoever builds the rich client. That client shows every kind of fediverse content in one place, with a secondary "details" view of the raw object and how it reached us. Section 4 says what the server must keep for it.
Priorities, used throughout:
| Priority | Meaning |
|---|---|
| P1 | Breaks interop, or loses content the user would see. |
| P2 | Degrades the experience. |
| P3 | Nice to have. |
1. Where PrivaPub stands (checked in the code, 2026-10-01)
Already right
- Public addressing: all three spellings are recognised (
…#Public,as:Public,Public). - Undo: embeds its whole object, which Misskey needs; it fetches the object only when it isn't embedded.
- Content types:
- Served documents are
application/activity+json; charset=utf-8, which is on Lemmy's exact allowlist and is accepted by GoToSocial and Misskey. - WebFinger answers
application/jrd+json, and answers for the actor URL as well asacct:. Akkoma and Iceshrimp.NET require both.
- Served documents are
- SSRF guard:
- Its IPv6 rule is an allowlist (
2000::/3only), so IPv4-compatible::a.b.c.d, NAT64 and the other tricks behind Mastodon's GHSA-vwhj (Jul 2026) are already refused. - It caps fetches at 1 MB. Misskey caps at 256 KiB, so keep our own documents well under that.
- Its IPv6 rule is an allowlist (
- Attachments:
- Up to 16 are kept. Mastodon drops anything past 4; Pixelfed albums and Threads carousels go beyond it.
{type: Link}attachments are not mistaken for media.
- Deleted posts answer 410 with a
Tombstone. Actorpublishedis truncated to the day. Thewebfingerproperty (FEP-2c59) is on every actor. - Live check: the whole follow, post, reply, like, boost, DM, edit and delete set round-trips with GoToSocial
0.22.1 (
tools/pasture/).
Wrong today (P1, cheap)
-
summaryis read as a content warning on every object type. It is one only on aNote, or whensensitive: trueis set. Elsewhere it is something else:Sender What summaryholdsWordPress, WriteFreely, Ghost, NodeBB ( Article)a teaser or excerpt Mobilizon, Gancio ( Event)the date and address, or the whole description Funkwhale ( Audio)a line of hashtags Mbin ( Page)a short title plus tags Today all of these arrive hidden behind a warning, and marked sensitive.
-
Persona and group usernames are only lowercased. Mastodon accepts
[a-z0-9_]with.and-only inside the name; Misskey^\w([\w-.]*\w)?$, 128 characters at most. A persona named outside that is unreachable from either. -
Our JSON-LD context does not define
postingRestrictedToMods. Iceshrimp.NET runs full JSON-LD expansion and silently drops every undefined term. Any term we add later (quote,Emoji,EmojiReact,interactionPolicy,votersCount) must be defined inActivityPubRenderer.Context()in the same commit.
Smaller
- No
Vary: Accepton actor and object URLs, although they answer HTML or JSON depending onAccept. - Only
create-…andannounce-…activity ids dereference.follow-,like-,accept-andundo-…answer 404. That is harmless while objects are embedded, but every id should resolve.
2. Rules that hold across platforms
| # | Rule | What PrivaPub must do | P |
|---|---|---|---|
| W1 | url, icon, image, attributedTo, actor, tag, attachment and alsoKnownAs can each be a single value, an object or an array. The url array matters most: PeerTube video files, Funkwhale streams, and Bridgy posts, whose rel: canonical link is at://…. |
Parse every shape. For "open original", use the text/html Link. Keep every other Link as a media variant. |
P1 |
| W2 | summary is a content warning only on a Note, or when sensitive: true. |
See §1. On other types store it as an excerpt or description. NodeBB 4.16 honours a CW only on a sensitive Note. | P1 |
| W3 | content can be Markdown. PeerTube descriptions and comments carry mediaType: text/markdown. |
Render it, then sanitise. Keep source{content, mediaType}: Markdown, BBCode, text/x.misskeymarkdown (MFM). |
P1 |
| W4 | mediaType is missing or wrong: Bridgy media has none, Funkwhale hard-codes audio/mpeg, Gancio image/jpeg. Mastodon types every attachment Document. |
Infer it from the object or attachment type, then sniff it in the media proxy. | P1 |
| W5 | Thumbnails live in five places: attachment icon (Mastodon), object icon[] (PeerTube), object preview (Loops), image (Bridgy video; Ghost as a bare string; WordPress), and icon (Plume). |
Keep them all; choose one per kind. | P1 |
| W6 | Accept, Reject and TentativeAccept are not always follow answers. Friendica and Hubzilla use them as event RSVPs; Mobilizon answers a Join; GoToSocial answers interaction requests with result; Mastodon answers a QuoteRequest. |
Route on what the object is. | P1 |
| W7 | id is not the page a person opens. WordPress uses ?p=123, Ghost /.ghost/…, Bridgy /convert/ap/at://…. |
Link to url, never to id. |
P1 |
| W8 | Rich types are updated in place: PeerTube live state, WordPress (on every save), Mobilizon. A poll's counts are refreshed with an Update{Question} that changes nothing else. |
Apply Updates to every kind. An Update with no newer updated is a refresh, never an edit revision. Mastodon applies the same rule. |
P1 |
| W9 | A Delete can arrive before its Create, and relays and forwarders re-send old Creates. |
Keep tombstones so deleted posts stay deleted. When the deleter is not the author, confirm with the origin: 404 or 410 means deleted. | P1 |
| W10 | Time comes in seconds (Mastodon), milliseconds (Misskey, us), or with offsets. -00:00 means floating local time (Hubzilla events). duration is ISO 8601 (PT6299S). GoToSocial rejects a status whose updated is earlier than published. |
Parse all of these, and clamp future times. Order by arrival (PrivacyIds.Arrived). Never emit updated < published. |
P1 |
| W11 | Language may be in contentMap, in @context[].@language (Pleroma 2.9+), or a language{identifier,name} object (Lemmy, PeerTube). Akkoma replaces content with the first contentMap entry. |
Read all three. When sending, put content and the primary contentMap entry first and keep them equal. |
P2 |
| W12 | Alt text: name (Mastodon), summary (GoToSocial 0.20.0, Akkoma reads it first), or their *Map forms. Avatar and header alt is in icon.name/image.name, or summary on Mastodon. |
Read both; send name. |
P2 |
| W13 | "id": null objects (Akkoma); a Tombstone served with 200 as a soft delete (FEP-4f05: NodeBB, Discourse); 410 for deleted GoToSocial 0.22 statuses. |
Accept a null id inside an activity; never emit one. Any Tombstone, whatever the status code, means deleted. |
P2 |
| W14 | Size limits on the receiving side: Misskey reads at most 256 KiB, truncates text at 8192 characters, CW 512, poll choice 256, alt 512. GoToSocial takes emoji up to 100 KB. Peers cap fetches at about 1 MB. | Accept long posts from others. Keep our own documents small. | P2 |
| W15 | Hashtag name comes with or without #. Mastodon normalises with NFKC + lowercase (watch Turkish İ). Lemmy adds an automatic #<community> tag to every post. |
Normalise the same way; ignore Lemmy's automatic tag. | P3 |
3. Per platform
Mastodon: 4.7.2 (2026-09-15); mastodon.social runs 4.8 alpha; client api_versions.mastodon = 11
Emits
- Objects and activities:
- Objects:
NoteandQuestiononly. - Federated
Block. Add/Removefor pins, featured hashtags and featured collections (4.6, FEP-7aa9).Move.QuoteRequest, itsAccept/Reject(withresult), andDelete{QuoteAuthorization}.FeatureRequest/FeatureAuthorization(4.6).
- Objects:
- Threads:
contextis a dereferenceable collection of the thread (FEP-7888, threads started on 4.5+). - Interaction counts:
likesandsharescarrytotalItems. - Quotes:
quote, plusquoteUriand_misskey_quote,quoteAuthorization, andinteractionPolicy.canQuote. - Link previews: from 4.7, a
{type: Link, href}attachment names the link a card is made from (FEP-8967). - Attachments:
duration, andiconthumbnails. - Actors:
webfinger,featuredCollections,interactionPolicy.canFeature,attributionDomains;memorial,suspended,indexable,discoverable;- avatar and header alt text in
summary; - several keys allowed (4.6).
- Ids: new accounts get numeric actor ids (4.5). Remote renames are accepted, keyed on the actor
id(4.7).
Expects
- Fetched documents:
idequals the URL requested, exactly.- The type is
activity+json, orld+jsonwith the profile. @contextincludes the ActivityStreams URL.
- WebFinger: it loops back to the same
id. - draft-cavage signatures:
(request-target)includes the query string (4.3).digestis signed on POST andhoston GET.- The window is 12 h, with 1 h of skew.
- Mastodon no longer signs
Accept(4.6). Never require it.
- RFC 9421: verified by default since 4.5.0.
- A request carries one signature only.
- It must cover
@method,@target-uriandcontent-digest, and includecreatedandkeyid. - 4.7 double-knocks: it signs draft-cavage first, and if we answer 400 or 401 it retries with RFC 9421. A 400 for any other reason therefore triggers a retry we would fail.
- Mastodon answers 503 when it temporarily cannot fetch a key; that is a retry, not a failure.
- Edits: an Update counts as an edit only with a newer
updated. - Quotes: a post with no
canQuotecannot be quoted by Mastodon users at all. A quote without a valid stamp stays "pending", and only its fallback link shows. - Fetch all replies is always on (4.5). Mastodon crawls
repliesas its instance actor, up to 500 per post, and deletes its copy when a refetch answers 404. - Converted types:
Article,Page,Event,Video,AudioandImagebecome<h2>name</h2>+ summary + link. The body is dropped. OnlyMentiontags notify. Previews come from fetching the page; FEP-8967'spreviewis ignored.
Gaps
| Gap | P | Client surface |
|---|---|---|
Quotes: read all keys; verify the QuoteAuthorization (type, host, attributedTo, interactingObject, interactionTarget; GHSA-vg36 compared domains only); revocation; strip the .quote-inline fallback |
P1 | Status.quote{state, quoted_status}, quotes_count, /statuses/:id/quotes, quote/quoted_update notifications, api_versions.mastodon ≥ 7 |
Being quotable: emit canQuote; answer QuoteRequest with Accept{object: request id, result: stamp}; serve and revoke stamps |
P2 | Status.quote_approval, PUT /statuses/:id/interaction_policy |
| Polls (see §3 Misskey for vote shapes) | P1 | Status.poll, /polls/:id, /polls/:id/votes, poll notification |
Custom emoji on posts, names, fields and poll options, proxied, refreshed by updated |
P1 | Status.emojis, Account.emojis |
| Link attachments as the card source | P1 | Status.card |
Inbound Move with Mastodon's checks (target re-fetched, its alsoKnownAs lists the old account, 7-day lock); move each persona's follow |
P1 | Account.moved |
Re-run WebFinger when preferredUsername changes; key accounts on the actor id |
P2 | Account.acct |
Inbound Block: stop delivering, hide |
P2 | relationship.blocked_by |
Add/Remove featured (pins, tags) |
P2 | GET /accounts/:id/statuses?pinned=true |
| Remote like and boost totals | P2 | counts |
Publish context and a paged replies |
P2 | — |
FeatureRequest: send Reject (or implement FEP-7aa9) |
P3 | — |
GoToSocial: 0.22.1 (2026-07-20)
Emits
- Objects:
NoteandQuestiononly. - Interaction policies on every status:
canLike,canReply,canAnnounceandcanQuote, each split intoautomaticApprovalandmanualApproval.- Defaults: open on public and unlisted posts.
- On followers-only posts, liking and replying are limited to the author, followers and mentioned accounts, and boosting to the author.
canQuoteis author-only on every post (0.21).
- Since 0.21 it asks politely:
LikeRequest,ReplyRequestandAnnounceRequest, with the interaction ininstrument. The answer isAccept/Rejectwhoseresultis a*Authorization. The interaction then carriesreplyAuthorization(withapprovedByas the legacy fallback). - Keys and actors:
- The key id is
…/main-key(no#), and its document is a stub actor. - There is no
sharedInbox. hidesToPublicFromUnauthedWeb/hidesCcPublicFromUnauthedWeb,indexable.featuredholds URIs only, and changes to it are never announced, so read it instead.
- The key id is
- What it leaves out:
context;likes/shares;- attachment
width/height.
- Deleted statuses: 0.22 keeps a stub and answers 410.
Expects
- Signed requests: every GET and POST is signed, draft-cavage only, with RSA keys. No RFC 9421 in either direction.
- Key handshake: the instance actor and key documents must be served unsigned, or both sides deadlock fetching each other's keys. Ours are: SecureMode exempts the instance actor.
- Content-Type: an inbox POST must be
activity+json, orld+jsonwith the profile. Anything else gets 406. - Activities: one without an
idis dropped. A 400 is never retried. - Keys: a changed public key on refresh is refused. Never rotate keys silently.
- Interaction policies:
- Third-party GoToSocial servers drop replies that have no valid
replyAuthorization. - On followers-only GoToSocial posts, send
ReplyRequest/LikeRequestinstead of a bare Create or Like.
- Third-party GoToSocial servers drop replies that have no valid
- Rate limit: 300 requests per 5 minutes per IP, answered with 503 and
Retry-After. - Not accepted: top-level
Audio. - Timelines: its cached home timeline can miss new posts. Check a delivery by URI, not through its timelines; see CLAUDE.md, "Testing".
Gaps
| Gap | P | Client surface |
|---|---|---|
Store remote interactionPolicy (with GoToSocial's defaults); disable or mark actions |
P1 | GoToSocial-style Status.interaction_policy |
Send ReplyRequest/LikeRequest where approval is needed; handle Accept{result}/Reject; attach the authorization |
P1 | own: pending/approved/rejected on our own reply |
Honour 503 with Retry-After in delivery and in the proxy |
P1 | — |
Respect hides*FromUnauthedWeb on our public pages; emit it for personas (it suits the privacy design) |
P2 | — |
| Measure media size when proxying | P2 | MediaAttachment.meta |
| Only advertise the policies we enforce | P2 | — |
Misskey family: Misskey 2026.10.0, Sharkey 2025.4.7, Iceshrimp.NET 2026.1.2-beta, CherryPick 4.17
Firefish is dead (its site has answered 410 since February 2025).
Emits
- Text and quotes:
_misskey_contentplussource{content, mediaType: text/x.misskeymarkdown}(MFM).- The quote is
_misskey_quote/quoteUrl, plus aRE:fallback inside the content. - An empty CW is a zero-width space.
- Attachments: each carries its own
sensitive. - Emoji tags carry
_misskey_license. - Polls:
QuestionwithoneOf/anyOf. Counts are inreplies.totalItemsper option.endTimeandclosedare set; there is novotersCount.- A vote is
Create{Note{name, inReplyTo, to:[owner]}}, one per choice. - Every vote triggers an
Update{Question}.
- A vote is
- Reactions are
Like{content: "👍"|":name:", _misskey_reaction, tag:[Emoji]}.- One reaction per user per note; a plain like is
❤. - They are sent to the author and to the reactor's followers.
- One reaction per user per note; a plain like is
- Actors:
isCat,_misskey_summary,_misskey_followedMessage;_misskey_requireSigninToViewContents;_misskey_makeNotesFollowersOnlyBefore/HiddenBefore(seconds; a negative value is relative to now);vcard:bday,vcard:Address,backgroundUrl.
- Vanilla Misskey has no edits.
- Sharkey adds:
- edits;
- a FEP-e232 quote
Linktag (it deliberately leaves outquote); - a
repliescollection; hideOnlineStatus,noindex,enableRss,speakAsCat.- It sends contentless Likes only to Mastodon-like peers, so we always receive Like+content.
- Iceshrimp.NET adds:
EmojiReact, several per user, and:name@host:for remote emoji;- a FEP-7888
context; htmlMfm: true(FEP-c16b);- every
QuoteRequestis auto-accepted; Bite,pronouns.
- CherryPick adds: events on a plain Note (
startTime/endTime),deleteAt, and federated chat (_misskey_talk: true).
Expects
- Inbound signatures:
- draft-cavage over
(request-target) host date digest; - at most 300 s of skew;
- since 2026.10.0, the query string is included in
(request-target). - No RFC 9421 anywhere in the family.
- draft-cavage over
- Activities:
- An activity's
idmust be on the signer's host. - Activities forwarded on behalf of someone else are refused. A group must
Announce. - Misskey answers 202 even when it drops something, so errors stay invisible.
- An activity's
- Fetched documents: request URL = final URL =
id; ≤256 KiB;activity+jsonorld+json. - Actor collections must be on the actor's host.
- Visibility: Misskey recognises followers-only by the author's own
followersURL, matched exactly. Otherwise:- A "specified" (direct) note with no resolvable recipients that Misskey fetches by URL is stored as public. Circle objects must therefore never be served to an unauthorised fetcher. They aren't: 404.
- Groups: vanilla Misskey drops
Announce{Create}, so groups shouldAnnouncethe Note itself. We send both. - Reactions: must be
:name:with no host, plus an Emoji tag, or they fall back to ❤. - Article/Page titles are never shown in Misskey's web UI.
- Iceshrimp.NET:
- full JSON-LD expansion (see §1);
@graph,@reverseand@includedare refused;- every actor must resolve through WebFinger;
preferredUsernamemust be unique per domain. PrivaPub shares one name space across personas and groups, so this holds.
Gaps
| Gap | P | Client surface |
|---|---|---|
Emoji reactions in all three inbound forms: Like with content/_misskey_reaction, EmojiReact, and Dislike-as-un-like (Sharkey). Normalise :n:, :n@host: and n@host. Store per actor, emoji and activity, including reactions to remote posts. Undo by id. |
P1 | emoji_reactions and pleroma.emoji_reactions [{name,count,me,url,static_url}] (Phanpy reads the first); PUT/DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji |
Outbound reaction as EmojiReact{content: ":name:", tag:[Emoji]}. A plain Like stays a favourite. |
P2 | same |
Polls: per-option counts (fall back to _misskey_votes); votersCount null when absent; Update{Question} is a refresh; an inbound Note{name, inReplyTo: question} with no content is a vote, never a reply |
P1 | Status.poll |
Keep MFM source; let the sanitiser keep <span class="mfm-*" data-mfm-*> and <ruby> (FEP-c16b) |
P2 | text, content_type; the rich client renders MFM from the source |
Per-attachment sensitive; isCat and other actor extras; enforce requireSignin… and makeNotes…Before on our public pages |
P2 | own privapub.* |
Quotes: when we send one, send every key (quote, _misskey_quote, quoteUrl, quoteUri, a FEP-e232 tag, the RE: fallback) |
P2 | — |
Pleroma 2.10.2 and Akkoma 3.20.1
Emits
- Notes:
@contextwith the instance's ownlitepub-0.1.jsonldURL (never fetch it) and@language;source{content, mediaType: text/markdown};contextandconversationholding the same value;quoteUrl/quoteUri.
- Edit history:
formerRepresentations, an OrderedCollection of earlier versions. - Reactions:
EmojiReact{content: ":name:", tag:[Emoji]}, several per user; separate from Like. - Polls:
votersCount(Pleroma 2.10.1, Akkoma 3.20). A vote is aNote{name, inReplyTo, to:[], cc:[owner]}. - Pleroma only:
ChatMessage, sent only to actors withcapabilities.acceptsChatMessages;Listen{Audio};- outgoing
Blockis on by default.
- Akkoma only:
- local-only posts are addressed to
<base>/#Public, which is not public; - FEP-2c59.
- local-only posts are addressed to
Expects
- Pleroma's inbox guard answers 400 for unknown activity types:
Move,QuoteRequestandBiteare not on its list (develop, 2026-09-30). Treat that 4xx as final. - Signatures (Akkoma):
hostmust be signed and match; signatures up to 2 h old and up to 40 min in the future. - Activity ids must be at least 8 bytes.
- ObjectAgePolicy (default in both) delists anything older than 7 days, so
publishedmust be accurate. - Quotes: Pleroma's InlineQuotePolicy rewrites incoming quotes into "RT: url" text. Neither reads FEP-044f
quote, so sendquoteUriand_misskey_quoteas well. - Edits: a changed
nameis ignored on update.
Gaps
| Gap | P | Client surface |
|---|---|---|
Language from @context[].@language |
P1 | Status.language |
formerRepresentations → our revisions |
P2 | /statuses/:id/history |
context/conversation threading; parents and quotes we could not fetch, kept as URIs |
P2 | pleroma.context; akkoma.in_reply_to_apid, akkoma.quote_apid precedent |
ChatMessage in as a direct message (also needed for Lemmy, Mbin and PieFed); advertise acceptsChatMessages only once it is answered |
P1 (in), P3 (out) | visibility: direct, Conversations |
Listen, vcard:bday, backgroundUrl |
P3 | own |
Lemmy: 0.19.20 live (lemmy.ml); 1.0.0-beta.2 (2026-09-25) in beta since May
join-lemmy.org's federation page is out of date. Current Lemmy neither sends nor reads stickied or commentsEnabled
on a Page: pins live in featured, locks in Lock.
Emits
- Group:
summary(sidebar HTML) andsource(sidebar Markdown); a plaindescriptionin 1.0;sensitive;attributedTo→ the moderators collection;featured;postingRestrictedToMods;language[];- 1.0 adds:
manuallyApprovesFollowersfor private communities,discoverable: falsefor unlisted ones, and the community's post tags intag[]asCommunityPostTagwith colour slotscolor01–color10.
- Page (post):
name,contentandsource;- a link post is
attachment[0] = Link{href, mediaType}, withimageas the thumbnail; - 1.0 image posts are
Image{url, name}, wherenameis the alt text; language{identifier, name},audience,to: [community, Public];- an automatic
#<community>hashtag; - 1.0 adds a Mention of the community and
context.
- Note (comment):
distinguished. - Votes:
Like/Dislikeas{actor, object, audience}with noto/cc, and theirUndo. - Moderation:
- a
Deletewith asummaryis a mod removal (1.0 addswithReplies); itsUndorestores; Lock/Undo{Lock}(1.0 also on comments);- a ban is
Block{target, removeData, summary, endTime}(0.19 also sendsexpires); Add/Removeof featured posts and moderators;Update{Group}by the moderator Person;- 1.0 adds
WarnandResolve{Flag}.
- a
- Delivery: everything travels inside the community's
Announce. For a new post Lemmy also sends a compatibilityAnnounce(Page)with a synthetic id; deduplicate it. - Private messages:
ChatMessageon 0.19; a single-recipientNoteon 1.0. - Context: 1.0's
contextcollection is unpaged and every comment's own context URL returns the whole post thread. Group threads by the root post, never by comparingcontextstrings. - Feeds: multi-communities are 1.0's
type: Feedactors.
Expects
- Fetched documents: Content-Type exactly one of
activity+json,activity+json; charset=utf-8, orld+jsonwith the profile; andidequals the URL fetched. - Addressing:
- Create, Update, Lock, Delete and Block must carry both
toandccarrays. - A post's community is the first Group found in
to∪cc. - In a public community, the object, the activity and the Announce all include Public.
- Create, Update, Lock, Delete and Block must carry both
- Accepted types: Page, Article, Note, Video and Event become posts.
Questionis dropped. - Anti-spam: activities for a community are accepted only if a local user follows it.
- Actors: a Create, vote, moderation action or Flag must come from a Person, Service or Organization. A Group or Application actor fails to parse, so moderation comes from the moderator Person. Our Flags, sent by an Application instance actor, probably fail (unconfirmed).
- Flags: exactly one
to(community or site);objecta URL or an array; the reason insummaryorcontent. - Moderation trust: an action is trusted when it is on the community's or the object's host (FEP-fe34), or when its actor is in the moderators list Lemmy fetched.
- 1.0 hides a local user's post or comment in a remote community until that community Announces it back. A community we host must therefore announce to the author's own instance too.
- Refused: Lemmy does not accept an incoming
Announce(Page).
Gaps
| Gap | P | Client surface |
|---|---|---|
Dislike and its Undo: a vote ledger per object (actor, ±1, activity, relaying group, time) |
P1 | favourites_count = upvotes; own privapub.vote{score, up, down, mine}, POST …/vote |
| Announces of activities other than Create (votes, moderation): trust the inner activity when the object's own group signed the Announce; refetching every vote does not scale. Keep the origin refetch for Create and Update. | P1 | — |
Moderation state: removals (reason, by, at, cascade), locks, bans with endTime/removeData, featured, moderators, Update{Group} by a moderator |
P1 | removed posts hidden plus privapub.removed; privapub.locked (replying answers 422); pins as pinned=true |
Link posts: keep Link.href, the thumbnail image and alt text; build the card (Lemmy sends no title or description for the link) |
P1 | Status.card |
ChatMessage in and out (out only to Lemmy < 1.0 and Mbin; Note to everyone else) |
P1 | visibility: direct |
Outbound shape for Lemmy: both to and cc; the community in to; Public in the object, Create and Announce; votes and comments sent to the community inbox |
P1 | — |
Communities we host: announce to every follower instance including the author's; pick Announce(object) per peer by NodeInfo (as PieFed does) |
P1 | — |
Flags from a Service-typed reporter actor with to: [community]; the reporter stays anonymous |
P2 | — |
Warn → moderation_warning notification; Resolve{Flag} |
P2 | AccountWarning |
Remote communities: description, language[], private (locked), discoverable; post tags |
P2 | Account.locked, own privapub.flairs[] |
Serve our communities' collections the way Lemmy reads them: inline outbox of Announce{Create{Page}}, inline featured Pages, inline moderators. Lemmy does not page. |
P2 | — |
Feed actors |
P2 | group-like account |
Read 1.0 context, grouped by root post; cross-post detection by URL |
P3 | — |
PieFed 1.7.17 and Mbin 1.10.1
- PieFed sends Lemmy's set plus:
- posts:
commentsEnabled,stickied,nsfl,genAI,searchableBy,canQuote; - galleries of several Documents;
- polls in communities (
QuestionwithvotersCount), and Mobilizon-styleEvents; - flairs in two dialects;
- comments:
repliesEnabled(comment lock),answer, plus customChooseAnswerandPollVoteactivities; Move{object: post}between communities;Feedactors;- emoji Likes and
EmojiReactcount as upvotes.
- posts:
- How PieFed formats what it sends us:
- It chooses the format by our NodeInfo software name, so keep that accurate.
- Software it lists as microblogs gets a bare
Announce(URL). - A poll vote is recognised only when it is a
Note{name, inReplyTo}withoutpublished. - Batched Announces (FEP-1a11) go only to PieFed and Pylova.
- Mbin:
- Thread
summaryis "short title + tags", so it is not a CW. - On link threads
sourceis a plain URL string. commentsEnabledandstickiedare sent.- A Person's
Announcecounts as an upvote; Mbin reads thelikes/dislikes/sharescounts we publish. - Private messages are
ChatMessage. - The magazine outbox is empty.
- Mbin also auto-ingests Mastodon posts by hashtag and Announces them.
- Thread
- Gaps:
- P1: Lemmy's P1 set, W2, and tolerating
sourceas a string. - P2: galleries; community polls (vote without
published); postMove;repliesEnabled;nsfl; flairs; publishlikes/sharestotals. - P3:
genAI; accepted answers; batched Announces.
- P1: Lemmy's P1 set, W2, and tolerating
NodeBB 4.16, Discourse, Friendica
- NodeBB:
- A topic's first post is an
Articlewithname,summary= an excerpt andpreview; replies are Notes. - Categories are
Groups withoutfollowers. contextis a paged collection with an ETag digest; NodeBB refetches withIf-None-Match.- Since 4.15, an Announce of anything but a Create or a plain object is accepted only from Group actors.
- It sends
Move/Removeof a whole context (FEP-f15d) andAdd{post → context}(FEP-11dd).
- A topic's first post is an
- Discourse (plugin, semi-dormant): categories and tags are Groups. "Full Topic" mode makes the topic an
OrderedCollection used as
context. - Friendica (2026.05-1):
- Group accounts relay with
Announce(object). - Titled posts are
Page/Article; it sendsDislike. instrument{Service}names the software.- It sends
Followwith a post as the object, meaning "include me in this thread". Answer that withRejector ignore it, without an error.
- Group accounts relay with
- Gaps:
- P1: W2 for NodeBB Articles (
privapub.excerpt). - P2: Groups without
followers; Announces from an Application; context Move/Remove; pagedcontextwith ETag; Friendica's thread-Follow.
- P1: W2 for NodeBB Articles (
PeerTube 8.3.1 (2026-09-28)
Emits
The account sends Create{Video}; the channel (a Group) sends Announce{Video}, so deduplicate.
| Part of the Video | What it holds |
|---|---|
| Attribution | attributedTo: [Person, Group] (both, possibly bare URLs); audience = the channel |
| Description | Markdown in content with mediaType: text/markdown; summary is the CW (since 7.2) |
url[] |
A text/html watch page; per-resolution mp4 Links (height, width, fps, size, ffprobe codec types); HLS application/x-mpegURL (since 6.3 audio and video can be separate, with "0" as the audio-only resolution); torrent and magnet; a metadata JSON |
| Images | icon[]: thumbnails up to 1920 px; preview: storyboards |
| Captions | subtitleLanguage[] with VTT and HLS URLs |
| Chapters | hasParts |
| Playback and metadata | duration (ISO 8601), views, state, isLiveBroadcast, permanentLive, latencyMode, commentsPolicy, downloadEnabled, category, licence, language, support, uuid, embedUrl, originallyPublishedAt, schedules, aspectRatio |
| Sensitivity | SensitiveTag |
- Value meanings:
- Live now means
isLiveBroadcast && state == 1. commentsPolicy: 1 open, 2 closed, 3 needs approval.
- Live now means
- Other activities:
- Comments are Markdown Notes.
Viewcomes from the server's Application actor.Dislike;ApproveReply(FEP-5624, since 6.2);CacheFile(mirrors); playlists.
Expects
- Replies must:
- be Public;
- have non-empty
content, a validurlandpublished; - have an
idon the actor's host; - have an
inReplyTothat resolves to the video or one of its comments.
commentsPolicy2 rejects replies; 3 holds them until approved.- PeerTube signs its fetches.
- It drops followers that have been unreachable for about 7 days (8.2).
What Mastodon does with it: <h2>name</h2> + summary + link. The description is dropped and there is no
attachment. The player is a card whose iframe loads from the remote host, which our proxy rule forbids.
Gaps
| Gap | P | Client surface |
|---|---|---|
Store the whole Video: variants, thumbnails, storyboards, captions, chapters, duration, live state, views, commentsPolicy, licence, category, language, support, channel |
P1 | own privapub.video |
Play through the proxy: a MediaAttachment{type: video} pointing at a proxied muxed mp4 (a web-video file, or a fragmented file whose codec types include both audio and video); preview_url = a ~560 px icon; meta.original with width, height, frame_rate, duration |
P1 | media_attachments |
| The proxy answers Range requests and rewrites HLS playlists and caption URLs to proxied ones. A 720p file is about 0.9 GB, so stream it; never buffer. | P1 | — |
| A video card made from the object, without a remote iframe | P1 | Status.card{type: video} |
Reply rules: closed when commentsPolicy is 2; replies sent Public with a url; ApproveReply shows our reply as pending |
P1 / P2 | own |
Dislike counts; live state through Update; chapters and captions |
P2 | own |
Optionally, View sent from the instance actor (so it never names a persona) |
P3 | — |
Loops (1.0.0-beta.14) and Pixelfed (0.14.4)
- Loops:
- A video is a Note with one mp4
Documentwhoseurlis a string. The poster is in the object'spreview. Videos are vertical. - Its interactionPolicy follows GoToSocial's model.
- It sends
QuoteRequestandFeatureRequest. - A top-level post it accepts must be a Note with an mp4 attachment, from an instance its admin allowlisted, ≤ 100 MB, checked with a HEAD request. Our media must answer HEAD.
- A video is a Note with one mp4
- Pixelfed:
- Posts are a Note with attachments.
- It also sends
location: Place{name, latitude, longitude, country},commentsEnabled,capabilities, andcanQuote(0.14). - Stories are
Add{Story}with a bearcap only Pixelfed understands. - Pixelfed 0.14 does FEP-044f and FEP-8fcf.
- What Pixelfed accepts:
- Only
Notes, and a top-level post must have media. - Every attachment must be a Document or Image with a string
urland amediaTypein the instance's list. The default list is jpeg, png and gif only, and a single webp or avif attachment rejects the whole post.
- Only
- Gaps:
- P1 outbound: keep JPEG/PNG renditions with an explicit
mediaType. - P1 inbound: Loops'
previewposter. - P2: Pixelfed
location→ ownprivapub.place, display only and never re-federated;commentsEnabled: falsedisables replies. - P3: ignore
Add{Story}without an error; answerFeatureRequestwithReject.
- P1 outbound: keep JPEG/PNG renditions with an explicit
Long-form: WordPress plugin 9.3.1, Ghost 6, WriteFreely 0.17.2
FEP-b2b8 (draft) describes the shape: plain-text name, a summary teaser (≤500), full HTML content, image, and
a preview Note fallback.
- WordPress:
- Object type: an Article for a titled post, a Page for a page, otherwise a Note.
- Fields:
image(the featured image),preview,interactionPolicy.canQuote. - A CW is
sensitive+summary+dcterms:subject. idis?p=123, different fromurl.- The blog actor is a Group with
attributionDomains. - It sends an
Updateon every save, and signs with RFC 9421 first (9.3.0), falling back to draft-cavage after any 4xx. - It drops followers-only replies.
- Ghost 6 (its ActivityPub service is separate, built on Fedify):
- Article with
imageas a bare string andpreview; members-only parts removed. - It refetches every object signed, never applies remote Updates, and accepts only Note and Article.
- Public is addressed as
as:Public.
- Article with
- WriteFreely: Article when the body has a paragraph break.
previewreuses the Article's id, so never store it as its own post. It has no comments. - Gaps:
| Gap | P | Client surface |
|---|---|---|
An Article shown in the Mastodon API: content = name + teaser (summary, else preview.content) + a link to url; a card made from the object (title, description, image then icon, author, provider, date) |
P1 | Status.content, Status.card |
| The full sanitised HTML kept for a reader view | P1 | own privapub.article{title, html, cover, excerpt} |
Body images duplicated in attachment removed; attributedTo arrays resolved to the Person |
P2 | — |
Events: Mobilizon 5.2.4, Gancio 1.28, Friendica/Hubzilla/Forte
FEP-8a8e (draft) is the common reference.
- Mobilizon:
- Times and status:
startTime,endTime,timezone,status/ical:status,isOnline,draft. - Place:
location: Place{address: PostalAddress, latitude, longitude}. - Participation:
joinMode,participantCount,maximumAttendeeCapacity,remainingAttendeeCapacity,anonymousParticipationEnabled. - Comments:
repliesModerationOption,commentsEnabled. - Other:
category,contacts. - Attachments: the online link
Link{name: Website};PropertyValues undermz:keys; a bannerDocument. - The event is attributed to the Group.
- RSVP:
Join{object: event}with a stable id that can be fetched; Mobilizon answersAcceptorReject;Leave.
- Times and status:
- Gancio: a single Application actor;
locationis an array ofVirtualLocationandPlace; no RSVP. - Friendica, Hubzilla: RSVP with
Accept/Reject/TentativeAccept. Hubzilla creates events asInvite{Event}, with HTML inlocation.content, and-00:00for floating times. - FEP-8a8e: a server that does not handle joins answers
JoinwithIgnore. - Gaps:
| Gap | P | Client surface |
|---|---|---|
Store times, time zone and place in every one of those shapes; RSVP routing (W6); Invite{Event}; answer Join with Ignore until RSVP exists |
P1 | content = title + "date (zone) · place" + link; card with the banner |
| Capacity, join mode, online link, status, category | P2 | own privapub.event |
RSVP out (Join/Leave, stable ids), from the persona the user chose |
P2 | own …/rsvp |
Audio: Funkwhale 2.0.11, Castopod 1.15.5, Owncast 0.3.0
- Funkwhale:
Create{Audio}whoseurlis an array: a page link plus the stream, withbitrateandsize.- Also
duration,position,disc,album,licenseandimage;summaryis a hashtag line (W2). Listen{Track}.
- Castopod: an episode arrives as a link-only Note; the player comes from OpenGraph/oEmbed. Fetching the episode
with the podcast type gives a
PodcastEpisodewith transcript and chapters. - Owncast: a
Serviceactor; "go live" is a Note with a thumbnail. - Gaps:
- P1: pick the audio Link out of
url, sniff its type, apply W2. Surfaces asMediaAttachment{type: audio}withmeta.original.durationand the cover aspreview_url. - P2: duration, cover, album, position and licence (own
privapub.audio); the Castopod transcript and chapters. - P3:
Listenhistory; Owncast live state.
- P1: pick the audio Link out of
Books: BookWyrm 0.9.3, NeoDB 0.19.4
- BookWyrm:
- To non-BookWyrm servers it sends a review as an
ArticlenamedReview of "Title" (★★★★): …, a comment or quotation as a Note with the citation appended, and the cover as aDocument. - The fields
inReplyToBook,ratingandquotego only to BookWyrm servers.
- To non-BookWyrm servers it sends a review as an
- NeoDB:
relatedWith[]carries the item, the rating (out of 10), the review and the shelf;tag[]holds catalogue items ({type: "Movie", href, name, image}). - Gaps:
- P1: keep the Article
name,tagentries that are not Mention or Hashtag, and covers that have no blurhash. - P2: keep
relatedWith,ratingandinReplyToBookraw; a card for the item; ownprivapub.review{item, rating, scale}.
- P1: keep the Article
Threads, Flipboard, Bluesky via Bridgy Fed
- Threads:
- Unsigned GETs answer 404, so fetch it signed, as our instance actor.
- Quotes arrive as
_misskey_quote+ a FEP-e232 tag + anRE:fallback. - Polls are not federated.
- Threads users must be 18 or over, opt in, and live outside the EU.
- Threads blocks servers that ignore deletes or have no privacy policy.
- P1: process
Deletepromptly; publish a privacy policy and a minimum age.
- Flipboard:
- Each flip is a link-only Note: headline, a link with
utm_*parameters, and a Mention of the magazine. - Magazines are Groups that
Announcebare URIs. - P1: fetch objects that arrive as bare-URI Announces (we do).
- P2: a card, or the post reads as a bare headline; deduplicate by canonical URL with
utm_*stripped.
- Each flip is a link-only Note: headline, a link with
- Bridgy Fed:
- Actors are
https://bsky.brid.gy/ap/did:plc:…, with handles like@x.bsky.social@bsky.brid.gy(dots in the user part).alsoKnownAsholdsdid:values; there is nopublishedand the outbox is empty. - Posts: a
urlarray with anat://canonical link (W1); media withoutmediaType(W4); link embeds flattened intocontent; video thumbnails inimage; long-form as Article. - Bridging a persona:
- opt-in, by following the bot;
- the persona needs an
iconand must be at least 7 days old; - only public posts are bridged;
- opting out takes a
Blocksent to the bot, which we never send today.
- Privacy note: two personas created on the same day share the same day-truncated
published. That is weak, but a link.
- Actors are
- P2:
Account.created_atfallback; the Bridgy opt-out Block (see §6).
4. What the rich client needs from the server
The rule: never drop what arrived; keep it typed where we understand it and raw where we don't. The Mastodon API
keeps working for existing apps. Everything it cannot express goes under a privapub object on the same entities,
following the precedent of pleroma.* and akkoma.*, plus a few endpoints of our own.
4.1 One post, many kinds
Post gains a Kind and one typed payload per kind.
| Kind | From | Typed payload | Mastodon API view | privapub.* |
|---|---|---|---|---|
| note | everyone | — | as today | source (Markdown/MFM/BBCode), per-attachment sensitive |
| article | WordPress, Ghost, WriteFreely, NodeBB, BookWyrm, Bridgy | title, excerpt, cover, full HTML, preview |
content = title + excerpt + link; card from the object | article (reader view) |
| video | PeerTube, Loops, Bridgy, Owncast | variants, HLS, poster, storyboards, captions, chapters, duration, live state, licence, channel | proxied video attachment + card |
video |
| audio | Funkwhale, Castopod | stream variants, cover, duration, album, position, licence, transcript, chapters | audio attachment |
audio |
| event | Mobilizon, Gancio, PieFed, Friendica, Hubzilla, CherryPick | start, end, zone, place, online link, join mode, capacity, status | text + card | event (+ RSVP) |
| poll | Mastodon, Misskey, Pleroma, GoToSocial, PieFed | options, counts, multiple, end, closed, voters | poll |
— |
| link | Lemmy, PieFed, Mbin, Flipboard, Mastodon 4.7 | href, thumbnail, alt, author | card |
link |
| review | BookWyrm, NeoDB | item, rating, scale, shelf | text + card | review |
| page / thread | Lemmy, PieFed, Mbin, NodeBB | title, community, flair, lock, removal, score | content + card for links |
thread{community, flairs, locked, removed, score, distinguished} |
These cut across every kind:
| Feature | Mastodon API | privapub.* |
|---|---|---|
| Quote | quote{state, quoted_status} |
the URI when it could not be fetched |
| Emoji | emojis |
— |
| Reactions | emoji_reactions (and pleroma.emoji_reactions) |
— |
| Votes | — | vote{score, up, down, mine} |
| Interaction policy | GoToSocial-style interaction_policy |
— |
| Edit history | edited_at, /history |
— |
4.2 Linking things together
Every reference is kept as a URI even when the target could not be fetched: the parent, the quoted post, the
community, the channel, the book, the original url. The client can then link out where it cannot embed. This
follows the akkoma.in_reply_to_apid / akkoma.quote_apid precedent.
A link opens url (W7), never id. Media and thumbnails only ever go through the proxy, so the client never contacts
a remote host.
4.3 Details view ("nerd stats")
GET /api/privapub/v1/statuses/:id/provenance (and the same for accounts):
| Group | Fields |
|---|---|
| Raw | The object exactly as received, with its hash. Every later refetch and Update, with timestamps. The @context as sent (the namespaces show toot, misskey, litepub, lemmy, pt, mz, gts, fedibird, …). |
| Path in | How it arrived: direct Create, inbox forward, Announce (and by whom: community, channel, magazine, relay), backfill, or fetch on demand. Delivered to the personal or the shared inbox. |
| Trust | Signature scheme: draft-cavage (algorithm string, signed headers, query signed or not), RFC 9421, FEP-8b32 proof, or verified by refetch from origin. Key id and key type. LD signature present but ignored. |
| Time | published, updated, received, and their gaps (backdated posts, clock skew). |
| Extensions | Detected from properties: 044f quote, interactionPolicy, 7888 context, 8b32 proof, 521a assertionMethod, 8967 Link attachment, c16b htmlMfm, _misskey_*, searchableBy, formerRepresentations, FEP-1b12 audience. |
| Origin | Software and version from NodeInfo, with a family. This is for display only: FEP-0151 says the names are opaque, so never branch on them except where a peer does the same to us (PieFed). |
| Media | Variants with codec, fps, size and bitrate; infohashes, magnets and mirrors; licence. |
| Counts | Remote likes, boosts, views, downloads and dislikes as last seen, with the time. |
| Interactions | Policy as received, defaults applied, approval state, authorization URIs. |
| Moderation | Removals, locks and bans as received from a community, with reasons. |
GET /api/privapub/v1/instances/:host returns cached NodeInfo (metadata extras: Misskey themeColor/maxNoteTextLength,
Pleroma features[]/federation.mrf_policies), the instance API's icon and description, the delivery health our
circuit breaker sees, and which signature scheme worked.
software.name → family, for display:
| Family | software.name values |
|---|---|
| Mastodon | mastodon (glitch-soc shows +glitch in the version), hometown, fedibird, kmyblue |
| Misskey | misskey, sharkey, cherrypick, iceshrimp, firefish, foundkey, catodon |
| Pleroma | pleroma, akkoma |
| Microblog | gotosocial, takahe, hollo, mitra, snac, smithereen, ktistec, wafrn, bonfire, friendica, hubzilla, streams/forte |
| Threadiverse | lemmy, piefed, mbin, kbin, lotide, nodebb, discourse |
| Media | peertube, pixelfed, loops, funkwhale, owncast, vernissage, castopod |
| Publishing | wordpress, ghost, writefreely, plume |
| Events | mobilizon, gancio |
| Other | bookwyrm, neodb, forgejo |
| Bridges and relays | bridgy-fed, activityrelay |
5. Signatures, identity and transport
| Topic | State on 2026-10-01 | PrivaPub | P |
|---|---|---|---|
| RFC 9421 inbound | Mastodon accepts since 4.5. WordPress and Fedify sign with it first. GoToSocial, the Misskey family, Akkoma, Pleroma and Bridgy do not. | Verify RSA and Ed25519; content-digest (RFC 9530); one signature; created and keyid |
P2 |
| RFC 9421 outbound | Mastodon 4.7 double-knocks | draft-cavage first; RFC 9421 after a 401; remember per host | P2 |
| 400 vs 401 | A 400 or 401 makes Mastodon 4.7 and WordPress retry with the other scheme | 401 only for signature failures (we do this); 400 only for bodies that are really malformed | P1 (keep) |
| Temporary key failure | Mastodon answers 503 | We should answer 503 too, and treat a 503 as a retry in delivery | P2 |
| Keys | publicKey can be an array (Mastodon 4.6). FEP-521a assertionMethod Multikey is FINAL (Ed25519 z6Mk…). GoToSocial key ids have no # and point at a stub. |
Read all of these | P2 |
| Integrity proofs | FEP-8b32 eddsa-jcs-2022: JCS, no JSON-LD. Sent by Mitra, Streams, Hubzilla, Fedify and others; Mastodon verifies them from 4.7 |
Verify, so relayed or forwarded objects need no refetch | P2 |
| LD signatures | Mastodon still sends RsaSignature2017 |
Ignore, and refetch from origin (we do) | — |
| Query string | GoToSocial, Akkoma 3.20 and Misskey 2026.10 sign it; GoToSocial retries without it | Verify both ways | P1 |
hs2019 |
The algorithm comes from the key; some senders hash with SHA-512 | Try rsa-sha256, then sha512 | P2 |
| Move (FEP-7628, FINAL 2026-08-26) | See Mastodon | Inbound P1; outbound per persona P3 (never link personas) | P1 / P3 |
| Followers sync (FEP-8fcf) | Mastodon, Pixelfed, Fedify and WordPress | Send and honour Collection-Synchronization. It protects followers-only posts. |
P2 |
| Instance actor discovery | FEP-d556 (FINAL), FEP-2677 | Publish both | P3 |
| Relays (FEP-ae0c, FINAL) | Mastodon-style relays forward LD-signed Creates; LitePub-style relays Announce. GoToSocial 0.22 subscribes to relays. | Client for both styles; refetch or check an integrity proof. This is how a small server sees beyond its follows. | P2 |
| FASP | Mastodon 4.4+, behind a flag. Its data sharing pushes content to a third party. | Do not join the data sharing; maybe consume search and trends | P3 |
| Search consent | indexable (missing = false), discoverable, searchableBy (FEP-268d, which takes precedence) |
Honour all three; emit explicit false per persona |
P2 |
6. Choices the privacy design has to make
These change what PrivaPub reveals, so they are the owner's calls, not implementation details:
- Link previews.
- Building a card from the object, or from FEP-8967
previewdata, fetches nothing; we do that. - Fetching the linked page tells that site our server read the link. Cache per URL across personas, so no fetch ties a page to one persona; add jitter.
- Proposed admin switch: off / from the object only / fetch.
- Building a card from the object, or from FEP-8967
- Outbound Block.
- Blocks are never sent today.
- Bridgy Fed's opt-out requires one, and Mastodon, GoToSocial and Misskey federate blocks as a matter of course.
- Proposal: never by default; an explicit per-persona "tell their server" option.
attributionDomains/fediverse:creator. It ties a persona to a website. Opt-in per persona only.- PeerTube
View. Counting a view tells the origin a video was watched. If ever, send it from the instance actor. - Bridging to Bluesky. It is per persona; each persona needs its own avatar and 7 days of age. Same-day personas
share a
publishedday. - Emoji reactions and votes are public by nature on every platform that has them. The client should say so before the first one.
Sources
The full source lists, with versions and dates, were gathered by five research passes on 2026-10-01. The primary ones:
| Area | Sources |
|---|---|
| Mastodon | CHANGELOG.md (4.3.0–4.7.2); docs.joinmastodon.org (spec/activitypub, spec/security, spec/webfinger, client/quotes, client/collections, entities); the main source (note_serializer.rb, activity/{create,update,accept,delete,quote_request,move,like,flag}.rb, status_parser.rb, media_attachment_parser.rb, fetch_link_card_service.rb, verify_quote_service.rb, fetch_all_replies_service.rb, signed_request.rb); developer posts for 4.5, 4.6 and 4.7; GHSA-vwhj, GHSA-rwcw, GHSA-vg36; issue #39997 (spam, July 2026) |
| GoToSocial | Codeberg releases 0.18.0–0.22.1; docs/federation/* (interaction_controls, posts, actors, http_signatures, access_control); internal/typeutils, internal/ap, internal/federation, internal/transport; issues #4939, #1894, #4994, #4849, #4677 |
| Misskey family | misskey-dev/misskey develop ed9654b (ApRendererService, ApInboxService, ApNoteService, ApAudienceService, ReactionService, check-against-url); PR #16250; Sharkey develop effcecb; Iceshrimp.NET dev 926fcde (FEDERATION.md, LdHelpers.cs, HttpSignature.cs, NoteRenderer.cs); CherryPick develop |
| Pleroma, Akkoma | Pleroma develop cfeca7f (transmogrifier.ex, inbox_guard_plug.ex, ap_extensions.md); Akkoma develop (FEDERATION.md, CHANGELOG.md, nodeinfo_extensions.md) |
| Lemmy | LemmyNet/lemmy main f1476db and tag 0.19.20 (crates/apub/apub/assets/* fixtures, objects/src/protocol/*, activities/src/protocol/*, the pending-post migration); PRs #5856, #6152, #6401, #6409, #6466; issues #6281, #6343, #6346; activitypub-federation-rust fetch/mod.rs |
| Other threadiverse | PieFed 41cccb7 (FEDERATION.md, docs/activitypub_examples, docs/fep-1a11.md); Mbin cf95b04 (docs/05-fediverse_developers); NodeBB 260a0cc (src/activitypub/*); discourse-activity-pub; friendica FEDERATION.md |
| PeerTube | docs.joinpeertube.org/api/activitypub; Chocobozzz/PeerTube develop (object-to-model-attributes.ts, custom-validators/activitypub/*, process-*.ts); live fetches from peertube2.cpy.re, garr.tv (8.2.4), tube.tchncs.de |
| Pixelfed, Loops | pixelfed dev (Transformer/ActivityPub/Verb/*, Inbox/HandlesCreates.php, Helpers::verifyAttachments); loops-server main (FEDERATION.md, NoteWithVideoAttachmentValidator.php) |
| Long-form, events, audio, books | wordpress-activitypub trunk (class-post.php, class-signature.php); TryGhost/ActivityPub v1.2.13; writefreely posts.go; Mobilizon 5.2.4 converters; Gancio 1.28.2/2.0-beta; Funkwhale 2.0.x; Castopod 1.15.5; Owncast 0.3.0; BookWyrm 0.9.3; NeoDB docs/internals/activitypub.md |
| New entrants | fed.brid.gy/docs and bridgy-fed activitypub.py; live probes of threads.net, flipboard.com and bsky.brid.gy; engineering.fb.com (2024-03-21) |
| FEPs | codeberg.org/fediverse/fep at 2026-09-30: 044f, 1311, 1a11, 1b12, 2345, 268d, 2c59, 4f05, 521a, 5624, 5feb, 7458, 7628, 7888, 7aa9, 844e, 8967, 8a8e, 8b32, 8fcf, 9098, 9967, ae0c, b2b8, c0e0, c16b, d556, e232, ef61, f15d, f228, fb2a, fe34 |
| Signatures | SWICG "ActivityPub and HTTP Signatures" report and its RFC 9421 adoption tracker; SocialHub "RFC 9421 HTTP signatures in 2026" (Jan 2026) |