I added ActivityPub to my website so people can follow posts, reply, like, and send emoji reactions from their existing Fediverse accounts.
This time, I added ActivityPub to changkyun.kim. Now this site appears on the Fediverse as the actor @me@changkyun.kim.
What I wanted to do was simple. When I publish a blog post, I wanted people to be able to find it on places like Mastodon or Misskey, reply to it, and send likes or emoji reactions. I did not want to build separate login or signup features. I thought that if people reacted using the Fediverse accounts they already use, my site could receive those reactions and show them under the post.
It did not come together cleanly from the beginning. For a while, I tried implementing ActivityPub directly, and later I changed direction and decided to use Fedify. This post is a record of that process.
Put simply, ActivityPub is a collection of APIs and rules for exchanging, propagating, and interacting with activities that happen on a network, somewhat like email, according to a mutually agreed protocol.
W3C published ActivityPub as a Recommendation on January 23, 2018. The document describes it as a decentralized social networking protocol based on ActivityStreams 2.0.
https://www.w3.org/TR/activitypub/
At first, I thought I could just build the necessary endpoints myself. I thought it would be enough to let accounts be discovered through WebFinger, expose an actor document, and create an inbox and outbox.
In practice, I could build them one by one. But soon there was a lot to handle. I had to export posts as Article objects, and distinguish activities like Follow, Create, Like, Undo, and Delete. When sending activities to other servers, I also had to get HTTP Signature right, and I had to think about shared inboxes too.
Trying it myself was helpful. It gave me a feel for how actors, objects, and activities connect in ActivityPub. But to attach it to a personal website and keep operating it, the code I implemented directly seemed like it would grow too large.
So I adopted Fedify. Fedify is a TypeScript library for building ActivityPub server apps. On its official site, it is introduced as an ActivityPub server framework for building Fediverse server apps.
After integrating Fedify, the parts I needed to decide became a bit clearer. I just had to define who the actor of this site is, which posts to publish to the outbox, and how to store activities that arrive in the inbox.
Of course, the implementation did not disappear. Even now, there are tables for storing comments, reactions, and the following feed, and I also use Cloudflare Workers KV and Queue. Still, rather than writing code to match every part of ActivityPub myself, I was able to focus more on how to handle this site's content and reactions.
Currently, this site exports blog posts and app posts as ActivityPub Article objects. When someone follows @me@changkyun.kim, the site receives a Follow and sends back an Accept. Public posts from connected actors can also be viewed on /following/.
Under each blog post, there is a note explaining ActivityPub comments. You can paste the post URL into the search box of a client like Mastodon, open this post from the search results, and then write a reply.
That reply is just a normal reply on the remote server. On my site, I read the incoming Create activity from the inbox, check which post it is replying to, and then store it as a comment. On the page, I show the author's name, profile image, and the original post URL together.
The reason I liked this approach is that I do not have to build a separate login feature. The identity of the person writing the comment already exists on their own Fediverse server. My site does not create that identity again; it simply displays the reply that arrived under the post.
When a Like comes in, I store it as a like reaction. When an EmojiReact comes in, I store it as an emoji reaction. Under the post, I collect and display the emoji and their counts.
Not every server sends reactions in the same way. Emoji reactions in particular differ from server to server. So for now, I receive what I can, and only show confirmed values on the page.
This is similar to comments as well. I did not create new reaction buttons separately on my site. I just made it so that reactions someone clicks in their own Fediverse client also arrive at my site.
Along the way, I also considered atproto. I was not trying to compare which one is better or worse. Looking at what I wanted to do, ActivityPub was the more direct fit.
The atproto documentation explains PDS, Relay, and AppView as the main components. User data lives in the PDS, the Relay gathers change events from many PDS instances and re-broadcasts them, and AppView reads that data to build the actual app interface and features.
https://atproto.com/guides/overviewhttps://atproto.com/guides/the-at-stack
With atproto, I could also imagine a setup where I run my own PDS and deliver post data through a layer similar to the Bluesky network's Relay, or in older terms, something close to BGS. But in that case, the goal changes a bit. I did not want to build a new social network or app.
My goal was to deliver posts I already own, especially blog Articles, into the distributed Fediverse. It was enough if subscribers could conveniently receive them through their own Fediverse accounts, and send replies, likes, or emoji reactions.
This blog uses Nuxt Content. Almost all of the source, including the source code and the blog content, is in a git repository. The posts are Markdown files, and the deployment target is also built from this repository. This structure is as portable as I want it to be.
To do the same thing with atproto, I would need to prepare a PDS for myself and decide what collections and lexicons to use to represent the posts there. And that would not be the end of it. I would also need a client or AppView to actually read and display the collections delivered through the central network. I did not want to build that part too.
So for this work, ActivityPub was a better fit. My goal was closer to publishing existing posts as Article objects and making them subscribable and reactable from existing Fediverse clients.
Even after doing this work, the basic structure of this site remains the same. Posts remain as Nuxt Content files, and their URLs stay under my domain. What changed is that I can now receive again the reactions that happened outside the post itself.
When I was trying to implement it directly, there were more parts to adjust than I expected. After integrating Fedify, there were parts I could delegate, and I could also see more clearly which parts I needed to decide myself. The scope of attaching a personal website to the Fediverse became smaller by that much.
There are still things I need to keep an eye on while operating it. ActivityPub implementations differ a bit from server to server, and reactions do not always arrive in the same form. Still, at this point, the flow I wanted has been built. The posts stay on my site, and the conversation can continue through each person's own Fediverse account.