- cross-posted to:
- fediverse@lemmy.ml
0
- cross-posted to:
- fediverse@lemmy.ml
So, you're captivated by the fediverse—the decentralized social web powered by protocols like ActivityPub. Maybe you're dreaming of building the next great federated app, a unique space connected to Mastodon, Lemmy, Pixelfed, and more. The temptation to dive deep and implement ActivityPub yourself, from the ground up, is strong. Total control, right? Understanding every byte? Sounds cool!
But hold on a sec. Before you embark on that epic quest, let's talk reality. Implementing ActivityPub correctly isn't just one task; it's like juggling several complex standards while riding a unicycle… blindfolded. It’s hard.
That's where Fedify comes in. It's a TypeScript framework designed to handle the gnarliest parts of ActivityPub development, letting you focus on what makes your app special, not reinventing the federation wheel.
This post will break down the common headaches of DIY ActivityPub implementation and show how Fedify acts as the super-powered pain reliever, starting with the very foundation of how data is represented.Challenge #1: Data Modeling—Speaking ActivityStreams & JSON-LD Fluently
At its core, ActivityPub relies on the ActivityStreams 2.0 vocabulary to describe actions and objects, and it uses JSON-LD as the syntax to encode this vocabulary. While powerful, this combination introduces significant complexity right from the start.
First, understanding and correctly using the vast ActivityStreams vocabulary itself is a hurdle. You need to model everything—posts (Note, Article), profiles (Person, Organization), actions (Create, Follow, Like, Announce)—using the precise terms and properties defined in the specification. Manual JSON construction is tedious and prone to errors.
Second, JSON-LD, the encoding layer, has specific rules that make direct JSON manipulation surprisingly tricky:Missing vs. Empty Array: In JSON-LD, a property being absent is often semantically identical to it being present with an empty array. Your application logic needs to treat these cases equally when checking for values. For example, these two Note objects mean the same thing regarding the name property:// No name property{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Note", "content": "…"}// Equivalent to:{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Note", "name": [], "content": "…"}Single Value vs. Array: Similarly, a property holding a single value directly is often equivalent to it holding a single-element array containing that value. Your code must anticipate both representations for the same meaning, like for the content property here:// Single value{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Note", "content": "Hello"}// Equivalent to:{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Note", "content": ["Hello"]}Object Reference vs. Embedded Object: Properties can contain either the full JSON-LD object embedded directly or just a URI string referencing that object. Your application needs to be prepared to fetch the object's data if only a URI is given (a process called dereferencing). These two Announce activities are semantically equivalent (assuming the URIs resolve correctly):{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Announce", // Embedded objects: "actor": { "type": "Person", "id": "http://sally.example.org/", "name": "Sally" }, "object": { "type": "Arrive", "id": "https://sally.example.com/arrive", /* ... */ }}// Equivalent to:{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Announce", // URI references: "actor": "http://sally.example.org/", "object": "https://sally.example.com/arrive"} { /* ... */ });// Now GET /.well-known/webfinger?resource=acct:username@your.domain just works! { /* Handle follow */ }) .on(Undo, async (ctx, undo) => { /* Handle undo */ });// Define followers collection logicfederation.setFollowersDispatcher( "/users/{handle}/followers", async (ctx, handle, cursor) => { /* ... */ });.lhr.life/✔ Sent follow request to @@activitypub.academy.╭───────────────┬─────────────────────────────────────────╮│ Actor handle: │ i@.lhr.life │├───────────────┼─────────────────────────────────────────┤│ Actor URI: │ https://.lhr.life/i │├───────────────┼─────────────────────────────────────────┤│ Actor inbox: │ https://.lhr.life/i/inbox │├───────────────┼─────────────────────────────────────────┤│ Shared inbox: │ https://.lhr.life/inbox │╰───────────────┴─────────────────────────────────────────╯Web interface available at: http://localhost:8000/.lhr.life/r/2 │╰────────────────┴─────────────────────────────────────╯
Because outsourcing your core business processes is a bad idea. A fediverse app that relies on a library to do all the fediverse stuff is going to have a bad time. Not straight away, but eventually.
It looks like an open source project. Makes a hell of a lot more sense to implement using that than rolling your own? If it’s missing something you need, write it for them.
Writing your own implementation is extremely unlikely to differentiate your app/product. There is a spec, either you implement it and it works, or you don’t and it doesn’t. Using a maintained library is by far the better option.
Focus your time on the product you are building not the tech.
I’m not sure about that. Sometimes it’s more about properly applying libraries. Thinking of database handling or cryptography
Imagine you want to write a competitor to PostgreSQL and you start out by importing SQLite into your project and building on top of that. To you it seems like a good idea because you’ve never written a DB app before and the only DB you’ve ever seen before is SQLite. You’ll get a prototype real fast but you’ll never build a PostgreSQL equivalent because you never learned the foundational knowledge of how a DB works and because SQLite forecloses all the pathways you need to get there.
Same thing.
That’s a complete apple/oranges comparison. Fedify is an application framework.
Try “Imagine you want to write a competitor to {Mastodon/PixelFed/Lemmy/PieFed/Pleroma/WriteFreely}”, and see where your analogy takes you.
Not same thing. I’m not talking about a postgres competitor. The other two replies already phrased it pretty good IMHO. I don’t think that, when building a fedi app, impending AP is the core of your app.
Of course you wouldn’t use an existing database engine as the foundation of a new database engine. But you would use an existing database engine as the foundation of an ERP software, which is a vastly different use case even if the software does spend a lot of time dealing with data.
If I want to build an application I don’t want to reimplement everything. That’s what middleware is for. The use case of my application is most likely not to speak a certain protocol; the protocol is just the means to what I actually want to do. There’s no reason for me to roll my own implementation from scratch and keep up with current developments except if I’m unhappy with all current implementations of that protocol.
Of course one can overdo it with middleware (the JS world is rife with this) but implementing a communication protocol is one of the classic cases where it makes sense.
I’d argue this is more like “I want to build a competitor to spotify so let’s decide between using mariaDB or writing an SQL compliant database from scratch”
In your example, a database is the end goal and you can either start with a premade or make your own.
Here, a social media platform is the end goal. Activitypub is a very important part of it but it’s not the entire piece.
If we replace the parts of your analogy with the original your example would parse out to “I want to make a competitor to lemmies ActivityPub integration, so let’s start with fedify” which is not the same as the article states.
Now, should you re-impliment a protocol yourself or use a generic library is the real question. Both have their benefits. With option A you have full code ownership and can wrap your solution around your end goal without the issue of dealing with the original to get needed changes accepted. You don’t have to worry about code not written by or understood by you. With option B, you get a more robust and almost certainly more accurate implementation. Along with, for free, better integration with any service using the same library. Very useful for a federated service when talking about cross platform.
Both have many more positives and negatives of course and each person should decide on their own how to proceed.
My opinion? I think it’s usually best to own anything which could feasibly be understood by a single dev. Even if each dev doesn’t. Anything larger shouldn’t be internal in my strong opinion unless very good, specific reasons apply that makes an external solution impossible or increadibly difficult. Most negatives of an external library also apply at that point with enough time.