Vibefed kb-mastodon
git clone https://github.com/reiver/vibefed
T=$(mktemp -d) && git clone --depth=1 https://github.com/reiver/vibefed "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/kb-mastodon" ~/.claude/skills/reiver-vibefed-kb-mastodon && rm -rf "$T"
skills/kb-mastodon/SKILL.mdMastodon -- Complete Federation Reference
Overview
Mastodon is a free, open-source, decentralized microblogging platform -- the dominant software in the Fediverse and the de facto reference implementation for ActivityPub social networking. Created by Eugen Rochko (Gargron) in 2016. Built in Ruby on Rails with a PostgreSQL database, Redis for caching/queues, and a React.js frontend. Licensed AGPL-3.0.
Mastodon federates via ActivityPub (server-to-server). Because of its dominance, Mastodon's specific implementation choices have become de facto standards that other Fediverse software must accommodate, even when those choices deviate from or extend the ActivityPub specification.
1. JSON-LD @context Structure
Mastodon's
@context is a three-element JSON array:
[ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", { "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", "toot": "http://joinmastodon.org/ns#", "featured": { "@id": "toot:featured", "@type": "@id" }, "featuredTags": { "@id": "toot:featuredTags", "@type": "@id" }, "alsoKnownAs": { "@id": "as:alsoKnownAs", "@type": "@id" }, "movedTo": { "@id": "as:movedTo", "@type": "@id" }, "schema": "http://schema.org#", "PropertyValue": "schema:PropertyValue", "value": "schema:value", "discoverable": "toot:discoverable", "suspended": "toot:suspended", "memorial": "toot:memorial", "indexable": "toot:indexable", "attributionDomains": { "@id": "toot:attributionDomains", "@type": "@id" }, "Hashtag": "as:Hashtag", "Emoji": "toot:Emoji", "sensitive": "as:sensitive", "votersCount": "toot:votersCount", "blurhash": "toot:blurhash", "focalPoint": { "@container": "@list", "@id": "toot:focalPoint" } } ]
Namespace Definitions
| Prefix | URI | Notes |
|---|---|---|
| | Standard ActivityStreams |
| | Mastodon-specific extensions |
| | BUG: should be -- causes issues with strict JSON-LD processors |
| From | Public keys, signatures |
Key toot:
Namespace Terms
toot:
-- custom emoji typetoot:Emoji
-- pinned posts collection (toot:featured
,@id
)@type: @id
-- featured hashtags collectiontoot:featuredTags
-- boolean, whether to list in directorytoot:discoverable
-- boolean, consent for search indexing (FEP-5feb)toot:indexable
-- boolean, account suspension statustoot:suspended
-- boolean, memorial/deceased accounttoot:memorial
-- poll total voter counttoot:votersCount
-- BlurHash string for media placeholderstoot:blurhash
--toot:focalPoint
coordinates for image cropping[x, y]
-- domains allowed to usetoot:attributionDomains
(since 4.3)fediverse:creator
Key as:
Extended Terms
as:
-- hashtag tag typeas:Hashtag
-- locked account flagas:manuallyApprovesFollowers
-- account migration targetas:movedTo
-- content warning flagas:sensitive
-- account aliases for migrationas:alsoKnownAs
Key schema:
Terms
schema:
-- profile metadata field typeschema:PropertyValue
-- profile metadata field valueschema:value
Gotcha: The
schema: prefix maps to http://schema.org# (with hash,
no HTTPS). This is technically incorrect per schema.org's own context
(https://schema.org/). Strict JSON-LD processors may fail to resolve
these terms. Implementations should accept both forms.
2. Content-Type and Accept Headers
For Fetching ActivityPub Resources (GET)
Send the
Accept header as:
Accept: application/activity+json, application/ld+json
Mastodon uses HTTP content negotiation. If the
Accept header does not
include an ActivityPub media type, Mastodon serves the HTML profile page
instead of the JSON-LD representation.
Both of these are valid and equivalent:
application/activity+jsonapplication/ld+json; profile="https://www.w3.org/ns/activitystreams"
For Sending Activities (POST to inbox)
Set the
Content-Type header to:
Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"
Or equivalently:
Content-Type: application/activity+json
Gotcha: Some HTTP libraries default to
application/json or
application/x-www-form-urlencoded. Always set Content-Type explicitly.
Send the request body as a raw JSON string, not form-encoded.
3. HTTP Signatures
Mastodon requires HTTP Signatures on all incoming POST requests and optionally on GET requests (when authorized fetch is enabled).
Algorithm
RSA-SHA256 (RSASSA-PKCS1-v1_5 with SHA-256) using the actor's RSA private key. The corresponding public key is published in the actor document under
publicKey.
Signature Header Format
Signature: keyId="https://example.com/users/alice#main-key", headers="(request-target) host date digest", signature="<base64-encoded-signature>"
Required Signed Headers
For GET requests (when authorized fetch is enabled):
-- the HTTP method and path (e.g.,(request-target)
)get /users/bob
-- target hostnamehost
-- RFC 2616 date (must be within clock skew tolerance)date
For POST requests (always required):
(request-target)hostdate
-- mandatory for POST; Mastodon will reject POSTs without a signed Digest headerdigest
Digest Header
For POST requests, compute the SHA-256 hash of the request body:
Digest: SHA-256=<base64-encoded-sha256-hash-of-body>
The
digest header name must appear in the headers parameter of the
Signature header.
Key ID Format
The
keyId is typically {actor_uri}#main-key. Mastodon dereferences
this to fetch the actor document, extracts publicKey.publicKeyPem, and
verifies the signature.
4. Authorized Fetch (Secure Mode)
Enabled by setting
AUTHORIZED_FETCH=true (or LIMITED_FEDERATION_MODE=true)
in the Mastodon environment.
What Changes
- All GET requests to ActivityPub representations of public posts and profiles require HTTP signatures (normally these are anonymous)
- Linked-Data Signatures are no longer generated for public posts
- Suspended/blocked servers cannot fetch public content because they must identify themselves via signature
- Server-level and user-level blocks become more effective
Limitations
- Uses significantly more server resources (signature verification on every request)
- Causes compatibility problems with older software that does not sign GET requests
- Does not prevent determined adversaries -- a bad actor can set up a new instance to bypass blocks
- Authorized fetch is circumventable by design; it is a friction mechanism, not a security boundary
Limited Federation Mode
LIMITED_FEDERATION_MODE=true is a superset of authorized fetch. It
additionally restricts federation to manually approved servers only.
Administrators must explicitly allowlist each domain.
5. Supported Object Types
First-Class Types
| Type | Mastodon Representation |
|---|---|
| Regular status/toot |
| Poll status |
Converted Types (Best-Effort)
These types are accepted but not given full treatment:
| Type | Handling |
|---|---|
| Uses (or if no content) as status text; appends ; becomes CW; becomes thumbnail |
| Same conversion logic as Article |
| Same conversion logic |
| Same conversion logic |
| Same conversion logic |
| Same conversion logic |
Critical gotcha:
Article objects are not rendered with their full
HTML content the way Note objects are. The content is used to generate
status text, but formatting, structure, and richness are lost. Many
platforms (WordPress, WriteFreely, Ghost) send Article objects that
display poorly on Mastodon. Some platforms have switched to sending Note
instead for better Mastodon compatibility.
6. Poll Implementation (Question/Answer)
Mastodon represents polls as
Question objects (used as an Object type,
not as an IntransitiveActivity). Polls are wrapped in Create
activities like normal statuses.
Poll Structure
{ "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.com/users/alice/statuses/12345", "type": "Question", "content": "<p>What is your favorite color?</p>", "published": "2024-01-01T00:00:00Z", "endTime": "2024-01-02T00:00:00Z", "oneOf": [ { "type": "Note", "name": "Red", "replies": { "type": "Collection", "totalItems": 42 } }, { "type": "Note", "name": "Blue", "replies": { "type": "Collection", "totalItems": 58 } } ], "toot:votersCount": 100 }
Key Properties
-- array of options for single-choice pollsoneOf
-- array of options for multiple-choice pollsanyOf- Each option:
,type: "Note"
,name: "<option text>"replies.totalItems: <vote count> - Options have no
propertyid
-- ISO 8601 timestamp when voting closesendTime
-- ISO 8601 timestamp when voting actually closed (may differ from endTime)closed
(votersCount
) -- total number of unique voterstoot:votersCount
Voting Mechanism
Votes are sent as
Create activities where the object is a Note with:
exactly matching the poll option'snamename
pointing to the Question'sinReplyToid
addressing the poll authorto
For multiple-choice polls, Mastodon sends separate
activities
for each selected option from the same voter.Create
Poll Updates
Poll results are updated via
Update activities on the Question object.
Remote instances can re-fetch the poll to get current vote counts.
7. Edit / Update Activities
Mastodon has supported editing statuses since version 3.5.0 (March 2022).
How It Works
- An
activity is sent with the full updated object asUpdateobject - The
timestamp on the object signals that this is an edit (not a first-time creation)updated - Receiving instances check: if a status with that
already exists locally, theid
triggers an edit; otherwise it is treated as a new statusUpdate - Mastodon's
automatically converts aFetchRemoteStatusService
to anCreate
when the status already exists in the local databaseUpdate
Edit History
- Edit history is stored locally and viewable on the originating instance
- Edit history is not federated in the ActivityPub representation (as of early 2026, there is an open issue requesting this: #23292)
- The
entity in the REST API exposes edit historyStatusEdit
Limitations
- Remote instances only see the latest version of a post
- There is no standard way to fetch previous versions via ActivityPub
8. Announce (Boost) Activities
Outgoing Announces
When a user boosts a post, Mastodon sends:
{ "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.com/users/alice/statuses/67890/activity", "type": "Announce", "actor": "https://example.com/users/alice", "published": "2024-01-01T12:00:00Z", "to": ["https://www.w3.org/ns/activitystreams#Public"], "cc": ["https://example.com/users/alice/followers", "https://remote.example/users/bob"], "object": "https://remote.example/users/bob/statuses/11111" }
The
field is a URI string, not an inline object. The
receiving instance must dereference (fetch) the URI to get the full
status content.object
Incoming Announces
Mastodon accepts both:
as a URI string (standard pattern) -- fetches the originalobject
as an inline full object -- uses the embedded dataobject
When processing an incoming
Announce, Mastodon uses
ActivityPub::FetchRemoteStatusService to retrieve the original status
if needed.
Boost Counters
Statuses expose a
shares collection:
-- number of boosts receivedshares.totalItems- Since Mastodon 4.3, like and boost counts are exposed in ActivityPub Note objects
- Since Mastodon 4.4, favorite and boost counts match remote server values when available
Gotcha: Boost/like counts are inherently inconsistent across instances. Only the originating instance knows the true total. Other instances only see counts from their own users plus what has been federated to them.
9. Direct Messages (Mentions-Only Visibility)
Mastodon does not have a dedicated direct message ActivityPub type. "Direct messages" are simply
Note objects with restricted addressing.
Addressing Rules for Direct Messages
{ "type": "Note", "to": ["https://remote.example/users/bob"], "cc": [], "tag": [ { "type": "Mention", "href": "https://remote.example/users/bob", "name": "@bob@remote.example" } ] }
Requirements:
- No
(as:Public
) inhttps://www.w3.org/ns/activitystreams#Public
ortocc - No follower collection URI in
ortocc - Only specific actor URIs in
to - All actors in
/to
must have a correspondingcc
tagMention
Visibility Level Calculation
Mastodon calculates visibility from addressing, not from an explicit visibility field:
| Visibility | | | Mention tags |
|---|---|---|---|
| public | | followers collection | -- |
| unlisted | followers collection | | -- |
| private | followers collection | (no ) | -- |
| limited | specific actors | -- | Not all actors are Mentioned |
| direct | specific actors | -- | All actors are Mentioned |
Gotcha: The distinction between
limited and direct depends on
whether all addressed actors have corresponding Mention tags. If you
address actors in to without Mention tags, Mastodon treats it as
limited (not direct).
10. Custom Emoji Federation
Custom emoji are represented as
Emoji tags on objects:
{ "type": "Note", "content": "<p>Hello :coolcat:</p>", "tag": [ { "id": "https://example.com/emojis/coolcat", "type": "Emoji", "name": ":coolcat:", "icon": { "type": "Image", "mediaType": "image/png", "url": "https://example.com/system/custom_emojis/images/coolcat.png" } } ] }
Properties
:type
(from"Emoji"
)toot:Emoji
: shortcode including colons (e.g.,name
):coolcat:
:icon.type"Image"
: MIME type (e.g.,icon.mediaType
)image/png
: direct URL to the emoji imageicon.url
Behavior
- Mastodon scans
,name
, andsummary
for shortcode substrings and links them to the emoji imagecontent - Custom emoji from remote instances are visible in federated posts
- Administrators can see and copy emoji from federated instances
- Emoji updates and deletions do not reliably federate
- Custom emoji may not render in display names and bios in all contexts
11. HTML Sanitization
Mastodon sanitizes all incoming HTML to protect API client developers from unexpected markup.
Allowed Elements
a, abbr, audio, b, blockquote, br, code, div, em, i, img, li, ol, p, pre, source, span, strong, ul, video
Allowed CSS Classes
mention, hashtag, ellipsis, invisible, h-*, p-*, u-*, dt-*, e-*
(Microformats2 classes are preserved.)
Behavior
- Unsupported elements are stripped (content preserved, tags removed)
- Unsupported attributes are stripped
- Links (
) are kept only if the protocol is supported (http, https); other protocols are converted to plain text<a> - Mentions and hashtags are represented as
tags with<a>
orclass="mention"class="hashtag"
Gotcha: If your platform sends rich HTML (tables, headings, definition lists, details/summary, etc.), Mastodon will strip all of it. Only the plain text content inside those elements survives. Design your content to degrade gracefully.
12. Character Limits
Composing Limit
Mastodon's default composing limit is 500 characters. This is enforced client-side and in the REST API. Instance administrators can modify this by changing source code, but it is not a simple configuration option.
Receiving Limit
Mastodon accepts and displays posts longer than 500 characters from remote instances. The ActivityPub protocol has no character limit.
- Long posts are displayed with automatic truncation and a "Read More" link in the web UI
- Most Mastodon-compatible mobile clients also truncate with "Read More"
- The post content is stored in full -- no data is lost
Hard Size Limits (FEDERATION.md)
| Property | Limit | Behavior if exceeded |
|---|---|---|
| JSON-LD serialization | 1 MB | Activity rejected |
| Username | 2048 chars | Actor rejected |
| Display name | 2048 chars | Truncated |
| Profile note (bio) | 20 kB | Truncated |
| Profile fields | 50 fields | List truncated |
| Field name/value | 2047 chars | Truncated |
| Poll options | 500 | List truncated |
| Custom emoji shortcode | 2048 chars | Rejected |
| Media descriptions (alt) | 1500 chars | Truncated |
| Account aliases | 256 | List truncated |
| Attribution domains | 256 | List truncated |
13. Thread Fetching and Reply Dereferencing
inReplyTo Resolution
When Mastodon encounters a status with an
inReplyTo pointing to an
unknown URI, it fetches the parent status from the remote server.
This works recursively up the reply chain to build the thread context.
Same-Server Reply Fetching
Upon discovering a remote status, Mastodon fetches up to 5 replies from the same server as the original post. This helps fill in thread context but is limited in scope.
Reply Distribution Problem
Replies are sent to the parent post's instance but are not forwarded through follower networks automatically. This creates a fundamental gap:
- If A posts, B replies, and C replies to B: A's instance may never see C's reply unless C explicitly mentions A
- Each instance has an incomplete view of any conversation
- Reply counts, like counts, and boost counts differ across instances
Fetch All Replies (4.4+)
Mastodon 4.4 introduced a "fetch all replies" feature (disabled by default). When triggered:
- Paginates through the first layer of
collections from the source serverreplies - Collects reply URIs
- Launches background workers to fetch them
Mastodon 4.5 Improvements
- Fetch all replies is enabled by default
- Automatically checks for missing replies on page load and every 15 min
- New AsyncRefresh API: the
endpoint returns a/api/v1/statuses/:id/context
HTTP header when background fetch jobs are pending, allowing clients to poll for completionMastodon-Async-Refresh
14. NodeInfo Implementation
Discovery Endpoint
GET /.well-known/nodeinfo
Response:
{ "links": [ { "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0", "href": "https://mastodon.example/nodeinfo/2.0" } ] }
NodeInfo 2.0 Response
GET /nodeinfo/2.0
{ "version": "2.0", "software": { "name": "mastodon", "version": "4.5.0" }, "protocols": ["activitypub"], "services": { "outbound": [], "inbound": [] }, "usage": { "users": { "total": 12345, "activeMonth": 5678, "activeHalfyear": 9012 }, "localPosts": 678901 }, "openRegistrations": true, "metadata": { "nodeName": "Example Mastodon", "nodeDescription": "A Mastodon instance" } }
Note:
software.name is always lowercase "mastodon". Use this to
detect Mastodon instances programmatically.
15. Mastodon REST API Compatibility Layer
Mastodon's REST API has become a de facto standard. Many Fediverse platforms implement it (partially or fully) to allow Mastodon client apps to work with them:
| Platform | Mastodon API Support |
|---|---|
| Pleroma | Near-complete, with Pleroma-specific extensions |
| Akkoma | Near-complete (Pleroma fork), with additional extensions |
| GoToSocial | Well-implemented, works with major Mastodon apps |
| Firefish | Partial support (now discontinued) |
| Sharkey | Partial support (Misskey fork) |
| Pixelfed | Partial support for photo-related endpoints |
| Friendica | Partial support |
API Discovery Challenge
Different platforms support different subsets of the Mastodon API and add their own extensions. There is no standardized way to discover which endpoints a server supports. Client libraries like Megalodon abstract over platform differences.
16. Recent Version Changes (4.x Series)
Mastodon 4.3 (October 2024)
Federation-relevant changes:
: newattributionDomains
property fortoot:
author attribution on external articlesfediverse:creator- Like and boost counts now exposed in ActivityPub Note objects
field in non-Note objects treated as HTML (was plain text)summary- Grouped notifications in REST API
- Numeric actor IDs: new users get numeric-based ActivityPub URIs (preparation for future account renaming)
Mastodon 4.4 (July 2025)
Federation-relevant changes:
- Quote post display: incoming quote posts per FEP-044f rendered in
web UI; new
attribute on Status entityquote - RFC 9421 HTTP Message Signatures: experimental support for verification of incoming signatures (behind feature flag)
- FASP (Fediverse Auxiliary Service Providers): initial support for shared decentralized services (behind feature flag)
- Fetch all replies: new backend feature to fetch missing remote replies (disabled by default)
- Remote counts: favorite/boost counts match remote server values
Mastodon 4.5 (November 2025)
Federation-relevant changes:
- Quote post authoring: users can compose quote posts;
API parameter; new quote states (quoted_status_id
,blocked_account
,blocked_domain
)muted_account - Fetch all replies enabled by default: automatic background fetching of missing conversation participants
- AsyncRefresh API:
HTTP header for async background job status on context endpointMastodon-Async-Refresh - RFC 9421 standard: experimental flag removed; incoming HTTP Message Signatures now verified by default
Supported FEPs
- FEP-67ff: FEDERATION.md
- FEP-f1d5: NodeInfo in Fediverse Software
- FEP-8fcf: Followers collection synchronization
- FEP-5feb: Search indexing consent (
)toot:indexable - FEP-044f: Consent-respecting quote posts
- FEP-3b86: Activity Intents (Follow, Announce, Like, Object intents)
17. Common Federation Gotchas
JSON-LD Issues
-
schema.org base URI bug: Mastodon maps
toschema:
instead ofhttp://schema.org#
. Strict JSON-LD processors will fail. Workaround: accept both forms.https://schema.org/ -
Context order matters: Some implementations are sensitive to the order of items in the
array. Match Mastodon's order when possible.@context -
Unknown properties ignored: Mastodon silently ignores JSON-LD properties it does not understand. Your custom extensions will not cause errors but will not be processed.
Reply Propagation
-
Incomplete threads: Mastodon instances have fundamentally incomplete views of conversations. Replies are not forwarded upstream. Only the instance where a post originated has the full picture.
-
Counter inconsistency: Like counts, boost counts, and reply counts differ across instances. Never assume these numbers are authoritative on any instance except the originating one.
Content Handling
-
Article objects degraded: Mastodon does not render
objects with full fidelity. Long-form content platforms should consider sendingArticle
for Mastodon compatibility or accept that their content will be reduced to plain text + URL.Note -
HTML stripped aggressively: Tables, headings, definition lists, details/summary, horizontal rules, and other block-level elements are stripped. Only a minimal subset of HTML survives.
-
Hashtag/mention encoding: When a mention is present, hashtags in the Note activity can sometimes be replaced with mentions during federation delivery (the object itself retains correct tags when fetched directly).
Protocol Behavior
-
Block activity sent S2S: Mastodon sends
activities server-to-server when a user blocks a remote account, even though the ActivityPub spec defines Block for client-to-server only. Your implementation should handle incoming Block activities gracefully.Block -
Linked-Data Signatures disabled in secure mode: When
, Mastodon stops generating LD-Signatures on public posts. Implementations that rely on LD-Signatures for relay forwarding will break.AUTHORIZED_FETCH=true -
Clock skew sensitivity: HTTP signature verification checks the
header. If your server's clock is significantly off, signatures will be rejected.Date
Performance
-
Link preview DDoS effect: Every instance that receives a post with a URL will independently fetch the link preview (OpenGraph data). A post that goes viral can cause hundreds of concurrent requests to the linked website.
-
Inbox forwarding: Mastodon does not forward activities through inboxes the way the AP spec describes. Replies to your posts from third-party servers may never reach followers who are not on the replier's server.
18. WebFinger
Mastodon requires WebFinger for user discovery. The endpoint:
GET https://example.com/.well-known/webfinger?resource=acct:alice@example.com
Must return a JRD document with at minimum:
: thesubject
URIacct:
: array including a link withlinks
,rel: "self"
, andtype: "application/activity+json"
pointing to the actor URIhref
Mastodon uses
username@domain as the canonical user identifier. The
WebFinger lookup is fundamental to Mastodon's database design: every
remote actor is stored with a username and domain pair resolved via
WebFinger.
19. Actor Structure
Mastodon actors are
Person type (or Application for the instance
actor, Service for bot/automated accounts). Key properties:
-- the local username (before the @)preferredUsername
-- display namename
-- bio (HTML)summary
-- profile page URL (HTML)url
-- ActivityPub inbox endpointinbox
-- ActivityPub outbox endpointoutbox
-- followers collection URIfollowers
-- following collection URIfollowing
(featured
) -- pinned posts collectiontoot:featured
(featuredTags
) -- featured hashtagstoot:featuredTags
-- RSA public key for HTTP signature verificationpublicKey
-- shared inbox for efficient deliveryendpoints.sharedInbox
-- avatar imageicon
-- header/banner imageimage
-- array ofattachment
for profile fieldsschema:PropertyValue
(discoverable
) -- directory listing consenttoot:discoverable
(indexable
) -- search indexing consenttoot:indexable
(memorial
) -- deceased account flagtoot:memorial
(suspended
) -- suspension flagtoot:suspended
-- locked accountmanuallyApprovesFollowers
-- migration target actor URImovedTo
-- array of aliases (for migration verification)alsoKnownAs
(attributionDomains
) -- fediverse:creator domainstoot:attributionDomains
20. Supported Activities (Inbox Processing)
| Activity | Object Type | Mastodon Behavior |
|---|---|---|
| | Creates a new status |
| | Creates a poll status |
| Other | Best-effort conversion to status |
| / | Edits status (if present, since 3.5) |
| | Updates remote actor profile |
| | Deletes remote status locally |
| | Removes remote actor and content |
| Status URI | Creates a boost |
| Status URI | Creates a favorite |
| Actor URI | Sends follow request |
| | Confirms follow relationship |
| | Rejects follow request |
| | Unfollows |
| | Removes favorite |
| | Removes boost |
| | Removes block |
| Actor URI | Records block (non-standard S2S use) |
| Actor + objects | Reports content/user to moderators |
| Actor URI | Triggers account migration |
| Status to | Pins a status |
| Status from | Unpins a status |