Written 2026-10-01 from the original 2023 code, the decePubClient UI, a federation gap audit of commit `075c222`, research on .NET ActivityPub libraries, and the owner's decisions. Each phase ends in a tagged deploy plus verification; tick phases off here as they land.
- [x] P2 Mastodon client API: v1.4.0, deployed 2026-10-01; OAuth and the anonymous API verified on production, the signed-in API verified locally (no real-client login on production yet)
- [x] P3 Social features: v1.5.0, deployed 2026-10-01; media, proxy, blocks, mutes, bookmarks, pins and reports verified by tests and locally (no upload on production yet)
- [x] P4 Groups and privacy features: v1.6.0, deployed 2026-10-01; communities, circles and local-only located posts verified by tests. v1.6.1 adds the pasture (`tools/pasture/`): live interop with GoToSocial 0.22.1 passes all 25 checks, three runs in a row. Lemmy and a live Mastodon circle member are not run yet; the pasture has GoToSocial only
- [x] P5 Lose nothing: v1.7.0 to v1.9.1, deployed 2026-10-01; parsing, typed details, provenance, downvotes, tombstones, federated blocks, all checked live against GoToSocial. Book reviews and forum threads keep their raw form only (typed in P7 and P8)
It is a self-hosted, Pleroma-like microblogging server for privacy-minded individuals and small communities. It
federates with Mastodon, Pleroma/Akkoma, Misskey and GoToSocial (`AvatarServer` enum). Its distinguishing idea is
**one private login (RootUser) owning several public personas (Avatars), each a separate actor with its own keys**.
The rename SocialPub → PrivaPub happened in the commit that added the private avatar service.
Its privacy additions on top of the usual fediverse features:
- **Location-ranged posts:** `Post.Location` and `RangeKm` (5 km default), currently unused.
- **Contacts a persona chooses to share:** `Avatar.SharedPersonalContacts`, plus a root contact book (`ContactItem`).
- **Private notes:** about other accounts (`RootUserNote`) and on one's own avatar (`PersonalNote`).
- **Invitation-gated groups and DM groups.**
- **Signup without email:** email is only for password recovery.
The client (decePubClient) is a near copy of **Pleroma-FE plus Pleroma's admin panel**: a visibility picker, a
subject line, Plain/HTML/Markdown content types, media with alt text, boosts and likes, threads, mutes and blocks, data
import/export, and admin sections for users, reports, emoji, MRF policies and uploads. Today it shows mock data and
calls nothing.
Leftovers of the collAnon template, not intent:
- the discussion and confrontation resources;
- collAnon's invitation flow;
- QR scanning;
- the "collAnon support" mail sender.
The finishing pass reinterpreted the owner's never-written `Group` and `DmGroup` as an ActivityPub Group actor with an
invitation code and a direct-message conversation. The owner confirmed it on 2026-10-01, with groups split into communities
and circles (see Owner decisions).
**Status at HEAD:**
| Status | Features |
|---|---|
| Works | Accounts and JWT, several avatars, per-avatar actors and keys, posts, replies, CW, delete, DMs, groups and invitations, WebFinger, NodeInfo, signed inbox for Follow/Undo/Create/Delete/Update, delivery queue |
| Missing | Following remote accounts (no outgoing Follow), home/local/federated timelines, notifications, likes, boosts, visibility other than public, media, polls, edits by the author, profile Update federation, mutes, blocks, reports (Flag), Move, custom emoji, the location and contact features, admin and moderation, and a client that talks to the server |
**Persona separation is breached in three places today:**
- invitation signup names the avatar after the root username;
- logs pair root IDs with IP addresses;
- NodeInfo counts avatars as users.
## Where it stood
**Works:**
- WebFinger, including `acct:` for groups
- NodeInfo 2.0
- Person, Group and Application actors with SPKI keys
- Outbound cavage signatures (every fetch is signed, so authorized-fetch servers work)
- Inbound cavage verification, with key refetch and a key-owner check
- Shared inbox, durable delivery with backoff
- Follow with auto-Accept or manual approval for groups, and Undo
- Inbound public, group and DM Creates
- Delete and Update restricted to the author
- DMs both ways, with Mention tags
**Critical (security):**
- **S1, actor/key cache poisoning:** `RemoteActorService.Upsert` and `GetActorByKeyId` accept any document under its
own `id`, with no origin, `publicKey.id` or `owner` checks. Any remote actor can be impersonated.
- **S2:** no check that an object's `id` host matches its actor's.
- **S3, SSRF:** only string checks. Names resolving to private addresses, redirects and rebinding all get through, and
a request can trigger it before authentication.
- **S4:** unbounded fetch size, and 500s on unexpected content.
- **S5:** remote HTML is stored raw, with no format flag.
- **S6:** loose signature freshness.
- **S8:** DM conversation injection.
- **S9:** "private" groups federate as Public.
- **S14:** the "admin" username grants admin, and Swagger is open in production.
**Blockers:**
- **O1:** no outgoing Follow, so no home timeline.
- **O2, O3:** replies and mentions aren't delivered to their targets.
- **I1:** inbound replies and mentions are stored but invisible, and there are no notifications.
- **G1:** groups Announce the object URI instead of the activity, so Lemmy and other FEP-1b12 software see empty
communities.
- **K7:** no media, timelines, likes or boosts for local users.
**Important:**
- **Data:** no indexes or unique constraints, upsert races (K1-K4).
- **Inbox:** processing runs synchronously with no idempotency store (I9, I10).
- **Delivery:** one serial worker, and a single poisoned row can stall the queue (L1-L3).
- **Objects:**
- Edits and profile updates don't federate (O7).
- No 410 Tombstones (A7).
- Content warning without a title (O5).
- Titles are lost on Mastodon (O6).
- Visibility modes are missing (O4).
- **Interactions:** likes, boosts and reports are dropped (I2, I3).
- **Media:** inbound attachments are dropped, and there's no media proxy (I6, S13).
- **Actor profile:**
- Actor `url` serves JSON to browsers (A1).
- No `attachment` fields, Move or `alsoKnownAs` (A2, A3).
- No locked or undiscoverable accounts (A4).
- **Signatures:** inbound RFC 9421 and Content-Digest (H1, H2).
## Owner decisions (2026-10-01)
| Question | Decision |
|---|---|
| Client interface | **Mastodon client API**, so Tusky, Elk, Phanpy, Ivory and the official apps work. Each avatar is its own Mastodon account: at OAuth authorize, the logged-in RootUser picks the avatar the token is for. PrivaPub-only features (avatars, groups, contacts, range posts) stay on `/clientapi`. Moving decePubClient onto the Mastodon API is out of scope. |
| Personas | **Unlinkable to other users and servers.** The admin can still see the link in the database. No root IDs next to IPs in logs, NodeInfo counts nothing that links personas, blocks, mutes and notifications are per avatar, and invitation signup no longer names the avatar after the root username. |
| Location-ranged posts | **Local only, never federated.** Shown to local users within the radius, with coordinates rounded on storage. |
| Groups | **Per group, two kinds.** A *community* federates per FEP-1b12, Lemmy-compatible: it Announces the activity, uses `audience`, and accepts posts from non-followers. A *circle* is invitation-only: posts are addressed to the members collection, and objects are served only to signed requests from members. |
### Owner decisions on what PrivaPub reveals (2026-10-01, from `docs/INTEROP.md` §6)
| Question | Decision |
|---|---|
| Link previews | **The server fetches the linked page itself.** It does this only for public posts, after a short random delay, once per link for the whole server (a shared cache, so a fetch never points at one persona), and when the post arrives, never when someone reads it. Cards built from the post's own data are used first. The client never contacts the site. |
| Blocks | **Federated.** A persona's block is sent to the blocked account's server as `Block`, and an unblock as `Undo{Block}`. Their server enforces it too, and they can learn they were blocked. This replaces the earlier "blocks never federate" rule. |
| Website authorship (`attributionDomains`, `fediverse:creator`) | **Off.** It is never emitted, not even as a per-persona option, because it would publicly tie a persona to a website. |
| PeerTube views | **Never sent.** The remote video file is still downloaded through our proxy when it is played; that cannot be avoided without pre-downloading video. |
| Bluesky bridging (Bridgy Fed) | **Allowed per persona**, with a clear warning that the posts become far more widely copied. Leaving the bridge must work, through a federated `Block` sent to the bridge. Persona creation dates are moved back by a random number of days, so personas created the same day no longer share a date. |
| Reactions and votes | **Public, with a one-time notice.** The client tells each persona once, before its first reaction or vote, that these are public and visible as that persona. |
## Libraries (researched; no maintained .NET ActivityPub library exists, so Letterbook and Iceshrimp.NET both wrote their own)
| Area | Choice |
|---|---|
| AS2 / ActivityPub model | Own thin layer on `System.Text.Json.Nodes`: inbound helpers for single-or-array values, id-or-object, `type` arrays and the three Public forms; outbound builders with Mastodon's `@context`. No JSON-LD processing; fetch from the origin instead of verifying LD signatures. |
| HTTP signatures | Keep own draft-cavage (sign and verify). Add **NSign 1.2.5** (`NSign.AspNetCore`) to verify inbound RFC 9421 and Content-Digest. Always answer a bad signature with 401, never 500. |
| HTML | **HtmlSanitizer 9.x** with Mastodon's allowlist (tags `p br span a del pre code em strong b i u ul ol li blockquote`, attributes `href rel class`, microformat classes, a scheme allowlist, forced `rel=nofollow noopener noreferrer`) |
| Markdown | **Markdig 1.4**: `DisableHtml`, autolinks, custom inline parsers for `@user@domain` and `#tag` emitting Mastodon's h-card and hashtag markup |
| Media | **NetVips** (+NetVips.Native) for images: autorotate, strip EXIF/GPS, thumbnails. **Blurhash.Core**, **FFMpegCore** for video. Own `IMediaStore` (local disk now). Not ImageSharp 4, whose license key is enforced at build. |
| Jobs | Own Mongo job collection for both inbox processing and delivery: `FindOneAndUpdate` leases, a `Channel` wake-up, per-host circuit breaker, Mastodon's backoff (`n⁴+15+jitter`, 16 tries). No MassTransit (commercial from v9, no Mongo transport). |
| Tests | New xUnit project with fixture JSON captured from Mastodon, GoToSocial, Misskey, Lemmy and Akkoma; interop checked with Fediverse Pasture, verify.funfedi.dev and activitypub.academy |