Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Fedify: Building ActivityPub servers without th...

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

Fedify: Building ActivityPub servers without the pain

The slides for my talk Fedify: Building ActivityPub servers without the pain at FOSDEM 2026.

Avatar for Hong Minhee (洪 民憙)

Hong Minhee (洪 民憙)

January 31, 2026
Tweet

More Decks by Hong Minhee (洪 民憙)

Other Decks in Programming

Transcript

  1. Hong Minhee Creator of Organizer of Based in 🇰🇷 Seoul

    Find me at 洪 民憙 Who’s giving this talk? Fedify Hollo BotKit FediDev KR Hackers’ Pub hongminhee.org [email protected] dahlia
  2. Building an ActivityPub app? Easy, right? It’s just JSON over

    HTTP. There are specs. There are existing implementations. My original plan was to implement Hollo within two weeks. It ought to be easy! [1] 1. An ActivityPub-enabled single-user microblogging server. ↩︎
  3. Until you hit reality It’s not just JSON. It’s JSON-LD

    . Specs are under-specified. Implementations differ. Really differ. I ended up spending more than a month wrestling with the protocol and debugging interoperability issues. … So I decided to build a framework for ActivityPub first. Or so I thought. [1] 1. JSON with linked data context. It’s complicated. ↩︎
  4. Introducing Fedify It handles the protocol plumbing so you can

    focus on your application logic. Type-safe Activity Vocabulary WebFinger & NodeInfo support Multiple signature mechanisms Framework-agnostic (Hono, Express, Next.js, …) Scalable fan-out delivery Security built-in (SSRF protection, …) CLI tools for debugging OpenTelemetry integration An opinionated ActivityPub framework for TypeScript.
  5. JSON-LD makes everything complicated Same data, multiple representations — a

    value can be a string, an object, or an array No static types — you must check everything at runtime Context resolution required — property names expand to full URIs Every implementation interprets it differently — Mastodon, Misskey, Lemmy, etc. ActivityPub uses JSON-LD, not plain JSON.
  6. The same property, three different shapes "actor": "https://server.example/users/alice" "actor": {

    "type": "Person", "id": "https://server.example/users/alice", "name": "Alice" } "actor": [ "https://server.example/users/alice", "https://server.example/users/bob" ] Your code must handle all of these cases. All of these are valid actor values:
  7. Fedify abstracts it away const = await . (); if

    ( ) { const = .n } Consistent API — no matter how the data arrives Type-safe — TypeScript knows actor is Actor | null Automatic dereferencing — URIs are fetched transparently if needed IDE support — auto-completion, type checking, refactoring With Fedify, you just write: actor activity getActor actor name actor [1] 1. Cross-origin embedded objects are automatically re-fetched from their origin to prevent spoofing attacks. See also FEP-fe34 and Origin-based security model from Fedify docs. ↩︎
  8. Signing headaches Outgoing: Sign with sender’s private key Incoming: Verify

    against sender’s public key But which signature scheme? HTTP Signatures (draft-cavage-12) — de facto standard HTTP Message Signatures (RFC 9421) — official standard, not widely adopted Linked Data Signatures — for embedded proofs, but obsolete Object Integrity Proofs (FEP-8b32) — for embedded proofs, modern replacement, but not widely adopted Different servers expect different schemes. You need to handle all of them. … Unless you use Fedify, which supports all four automatically. Every ActivityPub request needs cryptographic signatures.
  9. Fedify handles it automatically . ("/users/{identifier}/inbox", "/inbox") . ( ,

    async ( , ) => { // Signature verified before this runs ✓ const = await . (); if ( ?. == null) return; const = . ( . ); if ( ?. !== "actor") return; await . ( . , . ); await . ( // Automatically signed { : . }, , // Recipient new ({ : . , : })); }); Incoming: Signature already verified when your handler runs Outgoing: ctx.sendActivity() signs automatically Double-knocking : Tries RFC 9421, falls back to draft-cavage Just write your business logic: federation setInboxListeners on Follow ctx follow follower follow getActor follower id object ctx parseUri follow objectId object type db addFollower follower id object identifier ctx sendActivity identifier object identifier follower Accept actor follow objectId object follow [1] 1. See Double-knocking from the ActivityPub and HTTP Signatures report. ↩︎
  10. The fan-out problem 10,000 HTTP requests? Your server will timeout.

    Some servers are slow. Some are down. Failed deliveries need retries. Memory pressure from queued requests. And there’s more complexity: Shared inboxes: Many servers support a single inbox for all users. You should send once per server, not once per user. Retry logic: Exponential backoff, max attempts, dead letters… Building this from scratch? Weeks of work. What happens when you post to 10,000 followers?
  11. Fedify scales delivery import { } from "@fedify/fedify"; import {

    } from "@fedify/redis"; import from "ioredis"; const = ({ : new (() => new ()), // … omitted for brevity … }); Automatic retry: Exponential backoff, up to 10 retries by default Shared inbox optimization: preferSharedInbox sends once per server Two-stage fan-out : Sends to queue immediately, workers deliver Multiple backends: Redis, PostgreSQL, AMQP, Deno KV, and more Configure a message queue, and Fedify handles the rest: createFederation RedisMessageQueue Redis federation createFederation queue RedisMessageQueue Redis [1] 1. Since Fedify 1.5.0. See the Optimizing activity delivery for large audiences docs. ↩︎
  12. Your framework, your database // Hono app.use(federation(fedi, (c) => c));

    // Express app.use(integrateFederation(fedi, (r) => r)); // Fastify fastify.register(fedifyPlugin, { federation }); // Next.js export default fedifyWith(federation)(); Works with any Request / Response framework Any ORM: Drizzle ORM, Prisma, Kysely, TypeORM, Knex.js, … Just provide: A key–value store for caching Fedify doesn’t dictate your stack. Just use it as middleware:
  13. CLI tools for debugging $ fedify lookup --authorized-fetch @[email protected] Person

    { id: URL "https://threads.net/ap/users/17841400921600159/", name: "Barack Obama", icon: Image { url: URL "https://scontent-icn2-1.cdninstagram.com/v/t51.2885-19/361742448_804303214579253_9097669498418482911_n.jpg }, summary: "<p>Dad, husband, President, citizen.</p>", url: URL "https://threads.net/@barackobama/", preferredUsername: "barackobama", publicKey: CryptographicKey { id: URL "https://threads.net/ap/users/17841400921600159/#main-key", owner: URL "https://threads.net/ap/users/17841400921600159/", publicKey: CryptoKey { … } }, inbox: URL "https://threads.net/ap/users/17841400921600159/inbox/", outbox: URL "https://threads.net/ap/users/17841400921600159/outbox/", following: URL "https://threads.net/ap/users/17841400921600159/following/", followers: URL "https://threads.net/ap/users/17841400921600159/followers/", endpoints: Endpoints { sharedInbox: URL "https://threads.net/ap/inbox/" } } Inspect any ActivityPub object—even on servers with authorized fetch:
  14. Ephemeral inbox server $ fedify inbox --follow @[email protected] --accept-follow "*"

    ✔ The ephemeral ActivityPub server is up and running: https://d6e4a0f89bdee7.lhr.life/ ✔ Sent follow request to @[email protected]. ╭───────────────┬─────────────────────────────────────────╮ │ Actor handle: │ [email protected] │ ├───────────────┼─────────────────────────────────────────┤ │ Actor URI: │ https://d6e4a0f89bdee7.lhr.life/i │ ├───────────────┼─────────────────────────────────────────┤ │ Actor inbox: │ https://d6e4a0f89bdee7.lhr.life/i/inbox │ ├───────────────┼─────────────────────────────────────────┤ │ Shared inbox: │ https://d6e4a0f89bdee7.lhr.life/inbox │ ╰───────────────┴─────────────────────────────────────────╯ No need to deploy anything—test your federation logic instantly. Spin up a temporary ActivityPub server to receive and inspect activities:
  15. Clean shutdown, no traces ⠙ Sending Delete(Application) activities to the

    1 peer... ✔ Server stopped. No ghost actors left on remote servers. Press Ctrl - C to stop—peers are notified automatically:
  16. Ghost chose Fedify They could have built it from scratch.

    Instead: “Rather than mass-reinventing the wheel, we’re using an existing framework called Fedify.” —Alright, let’s Fedify, Ghost ActivityPub Blog Built a separate ActivityPub service with Fedify Open-sourced under MIT license Contributing back improvements to Fedify If Ghost can ship faster with Fedify, so can you. Ghost—the publishing platform with millions of users—needed ActivityPub.
  17. Built with Fedify Hollo Single-user microblogging with Mastodon API compatibility

    Hackers’ Pub Developer community on the fediverse Encyclia ORCID researcher profiles, federated … and more projects building with Fedify every day.
  18. Start building today Create a new project: npx @fedify/cli init

    my-app Or add to existing project: npm add @fedify/fedify # or deno add jsr:@fedify/fedify Resources: fedify.dev fedify-dev/fedify #fedify:matrix.org discord.fedify.dev @fedify/fedify @fedify/fedify Thank you! @[email protected]