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