Skip to content

Federation Module — ActivityPub ​

Location: apps/api/src/modules/federation/

Aggregate roots: RemoteFollower, RemoteFollowing, RemotePost (+ apPublicKey/apPrivateKey on User)

ActivityPub bridge to the Fediverse (ADR-008), shipped in three increments:

  • v1 — read side: WebFinger, Actor, Outbox, NodeInfo. Regulus users discoverable + readable from Mastodon et al.
  • v2 — write side: signed Inbox; inbound Follow / Undo(Follow); outbound Create(Note) delivery to remote followers on every public post.
  • v3 — inbound follow + read: local users follow remote actors, cache their Create(Note) posts, and read a Fediverse feed in-app.

Identity Mapping ​

Public actor identifiers are acct:username@host (WebFinger style; host from FEDERATION_HOST, default api.rgls.uk). Actor URL: https://<host>/users/<username>. Internal UUIDs are never exposed — all federation routes key on username, and the acct mapping layer translates to internal ids. Each user gets a lazily minted, persisted RSA-2048 keypair (#main-key) for HTTP signatures.


Public Routes (domain root) ​

These live at the domain root, not under /api/v1 (excluded in main.ts), because the Fediverse expects well-known paths. No auth.

MethodPathDescription
GET/.well-known/webfinger?resource=acct:user@hostJRD discovery document
GET/.well-known/nodeinfoNodeInfo index
GET/nodeinfo/2.1Instance metadata (user/post counts)
GET/users/:usernameActivityStreams Actor (Person + publicKey)
GET/users/:username/outboxLast 20 public posts as Create(Note)
GET/users/:username/followersOrderedCollection (count only)
POST/users/:username/inboxSigned inbound activities (202)

Inbox handling ​

Every inbound POST is verified with HTTP-Signature over the raw body (verifyInbound in signing.ts) before parsing. Then:

  • Follow → upsert RemoteFollower + send signed Accept back.
  • Undo(Follow) → delete the follower row.
  • Accept → mark our outbound RemoteFollowing as accepted.
  • Create(Note) from an actor we follow → cache as RemotePost (content capped at 5000 chars).
  • Delete → drop the cached RemotePost.

App-Facing Routes (/api/v1/federation, JWT) ​

MethodPathDescription
POST/federation/followFollow a remote handle (user@host) — sends signed Follow
POST/federation/unfollowSend Undo(Follow) + delete the row
GET/federation/followingMy remote followings (+ accepted flag)
GET/federation/feed?limit=Cached posts from followed remote actors

Events Consumed ​

EventHandler
posts.createdIf the post is public (and author not banned), deliver Create(Note) to every remote follower's inbox (deduped by sharedInbox), each request HTTP-signed. Best-effort: failures never break local posting.

Invariants ​

  • Banned users 404 on all federation routes; outbound delivery skips banned authors.
  • Only visibility: 'public', non-hidden, non-deleted, non-scheduled posts appear in the Outbox or get delivered.
  • Post HTML content is escaped before embedding in Notes.
  • Inbox returns 202 and never trusts an unverified signature.

See also: /modules/posts, /modules/identity, ADR-008 in docs/architecture/.

Regulus — invite-only social-knowledge platform