2026-10-01 11:23:03 +02:00
# Federation
PrivaPub is an ActivityPub server written in C#. This document follows
[FEP-67ff ](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md ) and describes how it federates.
## Supported federation protocols and standards
- [ActivityPub ](https://www.w3.org/TR/activitypub/ ) (server-to-server)
- [WebFinger ](https://webfinger.net/ )
- [HTTP Signatures ](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures ), `rsa-sha256` / `hs2019` with RSA keys
- [NodeInfo ](https://nodeinfo.diaspora.software/ ) 2.0 and 2.1
2026-10-01 13:19:16 +02:00
## Tested against
- **GoToSocial 0.22.1, end to end.** It runs in a private network on the workstation (`tools/pasture/` ) and is driven
through its own client API. Checked both ways:
- follows, including to a locked account;
- public posts with a content warning;
- replies with notifications;
- likes and boosts;
- direct messages;
- edits and deletes;
- unfollow.
- **Mastodon, Misskey, Lemmy and PeerTube, by unit tests only.** The tests feed the parser documents written in each
server's shape. No live exchange with any of them has run yet.
2026-10-01 11:23:03 +02:00
## Supported FEPs
- [FEP-67ff: FEDERATION.md ](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md )
- [FEP-f1d5: NodeInfo in Fediverse Software ](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md )
- [FEP-2c59: Discovery of a WebFinger address from an ActivityPub actor ](https://codeberg.org/fediverse/fep/src/branch/main/fep/2c59/fep-2c59.md )
2026-10-01 14:01:57 +02:00
- [FEP-1b12: Group federation ](https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md ) (communities; see "Groups")
2026-10-01 11:23:03 +02:00
2026-10-01 14:01:57 +02:00
Planned (see `docs/ROADMAP.md` , phases P5 to P8, and the per-platform notes in `docs/INTEROP.md` ): FEP-044f (quotes),
FEP-9967 (polls), FEP-c0e0 (emoji reactions), FEP-9098 (custom emoji), FEP-7888 and FEP-f228 (threads), FEP-7628 (Move),
FEP-8fcf (followers synchronisation), FEP-5feb (`indexable` ), FEP-8967 (link attachments), FEP-521a and FEP-8b32 (keys
and integrity proofs), FEP-ae0c (relays).
2026-10-01 11:23:03 +02:00
## Actors
Every account is an *avatar* : one private login can own several, and they are deliberately unlinkable. Nothing in an
actor document, a collection, NodeInfo or a delivery relates two avatars of the same login.
| Thing | Address |
|---|---|
| Actor (Person, Group, Application) | `/peasants/{name}` (`/users/{name}` redirects) |
| Inbox | `/peasants/{name}/mouth` |
| Outbox | `/peasants/{name}/anus` |
| Shared inbox | `/human-centipede` |
| Followers / following | `/peasants/{name}/groupies` , `/peasants/{name}/stalking` |
| Objects | `/peasants/{name}/scribbles/{id}` |
| Activities | `/peasants/{name}/grunts/{id}` |
| Direct-message context | `/peasants/{name}/whispers/{id}` |
| Profile and post pages | `/@{name}` , `/@{name}/{id}` |
The names are the project's own and are stable; resolve actors through WebFinger, not by guessing a path.
- The key is `{actor}#main-key` , RSA 2048, served as SPKI PEM with `owner` set to the actor.
2026-10-01 17:39:58 +02:00
- `published` on an actor is a whole day, chosen at random up to two weeks before the account was made, so two
personas made on the same day do not share a date. `indexable` is `false` .
2026-10-01 11:23:03 +02:00
- The instance actor is `/peasants/privapub` (type `Application` ). It signs every fetch PrivaPub makes, so no avatar's key
is used to read another server's content.
## Groups
A group is either a **community** or a **circle** .
2026-10-01 12:30:57 +02:00
- A **community** follows [FEP-1b12 ](https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md ). Posts
addressed to it (in `to` , `cc` or `audience` ) are accepted according to its posting policy (followers, anyone, or
moderators only) and the group `Announce` s the whole activity, with `audience` set, to its followers. A new post is
also announced as an object so Mastodon shows it. Updates and deletes of community content are announced too. A
top-level post is a `Page` with a `name` . Members are counted at `/flock` ; moderators are listed at `/wardens` , which
2026-10-01 12:31:13 +02:00
the actor's `attributedTo` points to, with `postingRestrictedToMods` as Lemmy expects. A mention of a community posts into it.
2026-10-01 12:30:57 +02:00
- A **circle** is private. Its actor is not discoverable and every follow is a request. Its posts are addressed to the
circle and its members collection, delivered to each member's own inbox and never announced. They are served only
to a signed request from a member, or from the instance actor of a member's server; anyone else gets 404. A
Mastodon member's replies reach only the people they mention.
- Announces from **remote** groups (Lemmy communities) are followed through to the activity: the object is fetched
from its own origin, never taken from the announce.
2026-10-01 11:23:03 +02:00
## Activities
Received:
| Activity | Effect |
|---|---|
| `Follow` | follows an avatar or community; `Accept` is sent unless the community approves members by hand |
2026-10-01 11:40:02 +02:00
| `Accept{Follow}` , `Reject{Follow}` | completes or ends a follow an avatar requested |
| `Undo{Follow, Like, Announce}` | reverses it |
2026-10-01 17:58:22 +02:00
| `Create{Note, Article, Page, Question, Video, Audio, Event, ChatMessage, …}` | stored when a local avatar follows the author, is addressed or mentioned, when it replies to a local post, or when it is addressed to a community the author follows; a public parent is fetched to complete the thread |
2026-10-01 11:23:03 +02:00
| `Update{Note}` | replaces the content; the previous version is kept |
| `Update{Person}` | refetches the actor |
2026-10-01 11:40:02 +02:00
| `Like` | counted and notified, on posts the liker could see |
2026-10-01 17:58:22 +02:00
| `Dislike` | counted as a downvote (Lemmy, PieFed, Mbin, Friendica); `Undo` takes it back |
2026-10-01 18:39:18 +02:00
| `EmojiReact` , `Like` with an emoji `content` | an emoji reaction (FEP-c0e0; Pleroma, Akkoma, Iceshrimp.NET, Misskey, Sharkey), Unicode or a custom emoji from its `tag` ; a `Like` whose content is ❤ stays a favourite; `Undo` takes it back |
2026-10-01 17:58:22 +02:00
| `Join` | answered with `Ignore` : PrivaPub hosts no events yet (FEP-8a8e) |
2026-10-01 11:40:02 +02:00
| `Announce` | counted and notified for local posts; shown to followers of the announcer, with the original refetched from its origin |
2026-10-01 17:58:22 +02:00
| `Delete` | deletes the object, or the actor and its follows; a deleted object id is remembered for 90 days, so a late `Create` cannot bring it back |
2026-10-01 12:22:08 +02:00
| `Flag` | becomes a report for this server's moderators |
2026-10-01 11:23:03 +02:00
2026-10-01 18:39:18 +02:00
Sent: `Follow` , `Undo{Follow}` , `Create{Note}` , `Create{Question}` and poll votes, `EmojiReact` and its `Undo` , `Update{Note}` , `Update{Person}` , `Delete{Tombstone}` , `Accept{Follow}` ,
2026-10-01 12:22:08 +02:00
`Reject{Follow}` , `Like` , `Announce` and their `Undo` , `Flag` . A deleted post answers 410 with a `Tombstone` .
- **Attachments** are `Document` s with `mediaType` , `name` (alt text), `blurhash` , `focalPoint` , `width` and `height` .
Uploaded files have all metadata removed.
- **Pinned posts** are the actor's `featured` collection (`/trophies` ); `featuredTags` is `/tattoos` .
2026-10-01 17:39:58 +02:00
- **Blocks are sent.** A blocked remote account receives `Block` from the blocking account (and `Reject{Follow}` if it
followed); an unblock sends `Undo{Block}` .
2026-10-01 12:22:08 +02:00
- **Reports** are sent as `Flag` by the instance actor, never by the reporting account.
2026-10-01 11:23:03 +02:00
A `Create` 's `Note` carries Mastodon's `content` , `contentMap` , `summary` and `sensitive` , plus `Mention` and `Hashtag`
2026-10-01 18:35:20 +02:00
tags.
**Polls** (FEP-9967) go out as a `Question` with `oneOf` or `anyOf` , each option's count in `replies.totalItems` ,
`endTime` , `votersCount` , and `closed` once it has ended. Incoming votes are `Create{Note{name, inReplyTo}}` with no
content, one per choice, counted once per voter. Counts are refreshed with an `Update{Question}` at most every three
minutes. Our own votes on other servers' polls are sent the same way to the poll's author only, without `published` .
An incoming `Update` without a newer `updated` only refreshes counts and details; it is never recorded as an edit.
**Custom emoji** (`Emoji` tags) are read on posts, display names, bios and profile fields, at most 64 per object.
**Profiles** keep their header, profile fields, `manuallyApprovesFollowers` , `published` , `movedTo` , `indexable` ,
`memorial` and avatar and header descriptions. A post's title becomes `name` and is also the first, bold line of `content` , because Mastodon does not show
2026-10-01 11:23:03 +02:00
`name` . A content warning without its own text uses the title, or "Content warning".
2026-10-01 17:39:58 +02:00
Received `summary` is a content warning only on a `Note` or `Question` , or when `sensitive` is `true` . On an `Article` ,
`Page` , `Event` , `Video` or `Audio` it is an excerpt or description, kept as such and never hidden.
2026-10-01 11:23:03 +02:00
Visibility is expressed in `to` /`cc` the way Mastodon does it: public, unlisted, followers-only and direct. Inbound
followers-only posts are recognised by the author's own `followers` collection.
2026-10-01 18:43:39 +02:00
## Link previews
For a public post that links to a page, PrivaPub builds a preview card. It uses, in order:
1. a Link attachment's own `preview` (FEP-8967) or the post's own image, title and summary;
2. otherwise, the linked page, read once by the server between 0 and 60 seconds after the post arrives (never when
someone reads it), with an `Accept: text/html` request that reads at most 512 KB, through the same address checks as
every other fetch.
The page's OpenGraph and Twitter tags give the title, description and image; the result is cached per address for 7
days and shared by every account on the server. Images are served to clients only through PrivaPub's media proxy.
`Federation:FetchLinkPreviews=false` turns page fetching off.
2026-10-01 12:30:57 +02:00
## Local-only posts
Posts with a location (shown to nearby users of this server) never leave the server, in any form.
2026-10-01 11:23:03 +02:00
## Security rules a peer will notice
- **Signatures.** Inbox POSTs must be signed over `(request-target)` , `host` , `digest` and `date` (or `(created)` ). The
date may be at most one hour old and fifteen minutes ahead. A bad signature gets 401, malformed input 400, an accepted
activity 202, too many requests 429. Activities are processed after the 202.
- **Origins.** An actor document is accepted only from the address it names as its `id` . A key only if its actor lists
it with `owner` set to the actor and on the actor's origin. An activity's `id` , and any object it creates, updates or
deletes, must be on its actor's origin. An embedded object from another origin is fetched from that origin.
- **Fetching.** All fetches are signed by the instance actor. They go only to public addresses, follow at most three
redirects and read at most 1 MB.
- **HTML.** Received HTML is sanitised to Mastodon's allowlist.
2026-10-01 18:25:18 +02:00
- **Keys we cannot fetch for now.** When a sender's key cannot be fetched because its server timed out or answered
5xx, the inbox answers 503 with `Retry-After: 300` rather than 401.
- **Activity ids.** `Create` and `Announce` ids dereference. `Follow` , `Like` , `Block` and the `Accept` , `Reject` and
`Undo` that answer them do not, because serving them would reveal who follows, likes and blocks whom; they are always
sent with their object embedded.
2026-10-01 11:23:03 +02:00
- **Delivery.** Failed deliveries are retried with Mastodon's backoff (16 attempts). A host that keeps failing is paused,
starting at an hour and growing to a week.
## Known limitations
2026-10-01 18:35:20 +02:00
- Our own posts carry no custom emoji: emoji are received and shown, not offered.
2026-10-01 12:22:08 +02:00
- Remote media is fetched through this server's proxy when a local client displays it.
2026-10-01 11:23:03 +02:00
- Collections expose counts, not members.
- Only `rsa-sha256` -style keys are verified. RFC 9421 signatures are planned.