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. ↩︎
. 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. ↩︎
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.
"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:
( ) { 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. ↩︎
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.
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?
} 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. ↩︎
// 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:
✔ 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:
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.